Billing model
This page explains how billing is wired and the one rule that keeps it correct. For the setup, see Payments & subscriptions.
The webhook is the source of truth
Checkout happens in Stripe, not in your app. After payment, Stripe sends webhook events, and the handler at app/api/webhooks/stripe/route.ts writes the result to your subscriptions table. Your app reads subscription state from your own database, never from the browser. This matters because the client can be tampered with, and a checkout can succeed seconds after the user closes the tab.
Checkout (Stripe) -> webhook event -> update subscriptions table -> app reads statusThe lifecycle events
customer.subscription.updated, syncs the current status.invoice.payment_succeeded, marks the subscription active.invoice.payment_failed, marks it past due.
Self-service changes go through the Stripe billing portal, and the same webhook keeps your database in step.
Verify the signature
STRIPE_WEBHOOK_SECRET before trusting any event. An unverified webhook is an open door, so never skip this check.Gating access
Features are gated on the stored subscription status, not on a price ID in the client. Plans and their Stripe price IDs are resolved server-side in libs/pricingPlans.ts via getStripePriceId, so no real price IDs ship in the browser bundle.
