Skip to content
Launch Kit logo

Customise

Payments and access

Checkout is done. The webhook sets has_access. Your product still has to read that flag.

The path is already built:

Pricing button → Checkout session → Stripe → /api/webhook/stripe → profiles.has_access = true.

Connect Stripe first: Stripe. This page is what you do with access after that.

What you edit

  1. NEXT_PUBLIC_STRIPE_PRICE_ID in .env.local
  2. Plan name, price, and bullets in config.ts
  3. Webhook URL + STRIPE_WEBHOOK_SECRET
  4. A check on has_access in your product routes

You do not invent a new access flag. You do not rebuild Checkout.

Guest and signed-in checkout

Both work. If the customer is signed in, Checkout gets client_reference_id = their user id. If they are a guest, the webhook looks up profiles by email or creates an auth user, then grants access.

One-time vs subscription

ButtonCheckout defaults to mode="payment". Pass mode="subscription" for a recurring price. The webhook grants access on checkout.session.completed and invoice.paid, and revokes it on customer.subscription.deleted.

Leave the button as it is. Create a one-time price in Stripe. Lifetime access stays true until you change it.

Gate the product

/dashboard is signed-in only. It does not check has_access. The page tells you to add that check. Do it in the dashboard layout or page:

ts
const { data: profile } = await supabase
  .from("profiles")
  .select("has_access")
  .eq("id", user.id)
  .single();

if (!profile?.has_access) {
  redirect("/#pricing");
}

apiClient already toasts “Pick a plan to use this feature” on a 403. Return 403 from paid API routes when has_access is false.

Gate the dashboard

Billing

ButtonAccount on the dashboard opens the Stripe customer portal. Enable the portal in Stripe. It only works after checkout has stored customer_id.