Billing
Billing runs on Stripe — checkout for new subscriptions, the hosted customer portal for managing existing ones, and webhooks to keep your database in sync.
The flow
| Action | How |
|---|---|
| New subscription (free → paid) | Stripe Checkout |
| Manage / switch plan (existing subscriber) | Stripe customer portal |
| Plan state in your DB | Updated by webhooks only |
Key routes
| Route | Role |
|---|---|
app/api/billing/checkout | Creates a Stripe Checkout session. |
app/api/billing/portal | Opens the customer portal. |
app/api/billing/webhook | Receives Stripe events and updates the plan. |
lib/billing/stripe/* | Stripe client + handlers. |
lib/billing/plans.ts | Plan tiers & prices (source of truth). |
The plan only changes when a webhook arrives. Checkout completing in the
browser is not enough — the customer.subscription.created / .updated event
must reach /api/billing/webhook. If webhooks aren’t wired, real payments
succeed but the user’s plan never updates.
Local development
Forward webhooks with the Stripe CLI, bound to your app’s key:
stripe listen --api-key "$STRIPE_SECRET_KEY" \
--forward-to localhost:3000/api/billing/webhookCopy the printed whsec_… into STRIPE_WEBHOOK_SECRET and restart. Full
walkthrough in Your first run.
Production
In production you register the webhook endpoint in the Stripe dashboard and enable the portal’s plan-switching feature once. See Stripe in production.
Changing prices
Edit lib/billing/plans.ts, create matching Stripe prices, and point
STRIPE_PRICE_PRO / STRIPE_PRICE_TEAM at the new ids. See
Plans & limits.