Technical Reference
Stripe Billing: Technical Reference
Stripe Checkout, Customer Portal, webhook signature verification, and subscription state.
Stripe Billing: Technical Reference
Author: navi-ops doc-updater
How it works
Billing is implemented as authenticated Next.js API routes plus a webhook endpoint:
- Client:
BillingClient.tsxcalls checkout/portal APIs. - Server routes: create checkout sessions, return subscription status, open portal sessions.
- Webhook: handles Stripe lifecycle events and upserts tenant subscriptions.
- Billing library: shared plan/status helpers and DB upsert/cancel logic.
Key files
src/app/dashboard/billing/BillingClient.tsx, billing UI and calls to/api/billing/*src/app/dashboard/billing/page.tsx, server page; loads initial subscriptionsrc/app/api/billing/checkout/route.ts, creates Stripe checkout sessionsrc/app/api/billing/portal/route.ts, creates Stripe customer portal sessionsrc/app/api/billing/status/route.ts, returns tenant subscription snapshotsrc/app/api/billing/webhook/route.ts, processes Stripe webhook eventssrc/lib/billing.ts, plan ranking, placeholder mode, upsert/cancel helperssrc/lib/validate.ts,BillingCheckoutSchemasrc/db/schema.ts,subscriptionstable
Database details
subscriptions table:
tenant_id(unique FK)stripe_customer_idstripe_subscription_idplan(free|pro|team)seatsstatus(active/past_due/canceled/trialing)current_period_end- timestamps
API endpoints
POST /api/billing/checkout(auth), body{ plan }, creates Stripe Checkout URLPOST /api/billing/portal(auth), returns Stripe billing portal URLGET /api/billing/status(auth), returns{ plan, status, seats, currentPeriodEnd }POST /api/billing/webhook(public), handles:checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deleted
Tenant isolation
Authenticated billing routes resolve tenant via getTenantId(req) and read/write only that tenant’s subscription row (subscriptions.tenant_id = tenantId). Webhook tenant resolution uses Stripe metadata (tenantId) and fallback lookup by stripeSubscriptionId when metadata is absent.
Implementation notes
BillingCheckoutSchemaacceptsstrategos|archon|pro|team; server logic mapspro/teamto Stripe price IDs.isStripePlaceholderMode()bypasses Stripe API calls for test/degraded envs.- Webhook signature validation is enforced unless webhook secret is placeholder/missing.
- Team seats are normalized using
getTeamSeatCount()and minimum constraints in webhook handler.
Ways to extend this
- Add yearly billing and explicit seat selection UX.
- Track billing audit events in a dedicated table.
- Harden plan-key mapping to remove dual naming between marketing labels and internal enums.