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
NEXT_PUBLIC_STRIPE_PRICE_IDin.env.local- Plan name, price, and bullets in
config.ts - Webhook URL +
STRIPE_WEBHOOK_SECRET - A check on
has_accessin 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.
Create a recurring price. On the pricing card, render:
<ButtonCheckout priceId={plan.priceId} mode="subscription" />Turn on the Customer Portal so people can cancel.
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:
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.