ShipKit

Payments

Set up Stripe payments and subscription management

Payments

This boilerplate uses Stripe for payments, supporting one-time purchases and subscriptions.

Configuration

1. Create a Stripe Account

  1. Go to stripe.com and create an account
  2. Get your API keys from Developers → API keys

2. Set Environment Variables

Add to your .env.local:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...

3. Create Products in Stripe

  1. Go to Products → Add product
  2. Create your pricing plans
  3. Copy the Price IDs

How It Works

Checkout Session

Create a checkout session to redirect users to Stripe:

// src/lib/stripe/actions.ts
"use server";

import { getStripe } from "@/lib/stripe/client";
import { createClient } from "@/lib/supabase/server";

export async function createCheckoutSession(priceId: string) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error("Not authenticated");

  const stripe = getStripe();
  const session = await stripe.checkout.sessions.create({
    customer_email: user.email,
    line_items: [{ price: priceId, quantity: 1 }],
    mode: "payment", // or "subscription"
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    metadata: {
      user_id: user.id,
    },
  });

  redirect(session.url!);
}

Webhooks

Stripe sends events to your webhook endpoint. The boilerplate handles:

  • checkout.session.completed - Payment successful
  • customer.subscription.updated - Subscription changed
  • customer.subscription.deleted - Subscription canceled
// src/app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  const event = stripe.webhooks.constructEvent(
    body,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  switch (event.type) {
    case "checkout.session.completed":
      // Handle successful payment
      break;
    case "customer.subscription.deleted":
      // Handle subscription cancellation
      break;
  }

  return new Response("OK");
}

Customer Portal

Allow users to manage their subscription:

export async function createPortalSession() {
  const stripe = getStripe();
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
  });

  redirect(session.url);
}

Database Schema

Subscriptions are stored in the subscriptions table:

create table subscriptions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users on delete cascade,
  status text not null,
  stripe_customer_id text,
  stripe_subscription_id text,
  stripe_price_id text,
  current_period_end timestamptz,
  cancel_at_period_end boolean default false,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Enable RLS
alter table subscriptions enable row level security;

create policy "Users can view own subscription"
  on subscriptions for select using (auth.uid() = user_id);

Hooks

Use the useSubscription hook to check subscription status:

import { useSubscription } from "@/hooks/use-subscription";

export function PremiumFeature() {
  const { isSubscribed, loading } = useSubscription();

  if (loading) return <Skeleton />;
  if (!isSubscribed) return <UpgradePrompt />;

  return <FeatureContent />;
}

Testing

Use Stripe test mode with these test cards:

CardScenario
4242 4242 4242 4242Success
4000 0000 0000 0002Declined
4000 0000 0000 32203D Secure

Local Webhook Testing

Use Stripe CLI to forward webhooks locally:

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks
stripe listen --forward-to localhost:3000/api/webhooks/stripe

On this page