Billing

Stripe Checkout + Billing Portal. Plan state mirrored to organizations.plan_status.

Files

  • src/lib/billing/stripe.ts — client + createCustomer, createBillingPortalSession, createCancelPortalSession.
  • src/lib/billing/plans.ts — plan metadata.
  • src/app/api/internal/billing/route.ts — checkout session creation.
  • src/app/api/webhooks/stripe/route.ts — inbound Stripe events.
  • src/app/overview/settings/billing/page.tsx — billing UI.
  • src/components/overview/upgrade-button.tsx — triggers upgrade.

Plans

From src/lib/billing/plans.ts:

Plan Price Emails/mo SMS/mo Overage
free $0 100 10 hard limit
pro $25 10,000 1,000 email $0.001, SMS $0.01
enterprise custom

Default on signup: free.

Upgrade flow

  1. User clicks Upgrade → POST /api/internal/billing.
  2. Server finds or creates a Stripe customer, persists stripe_customer_id on the org.
  3. Creates Checkout Session with STRIPE_PRO_PRICE_ID and:
    • success_url: /overview/settings/billing?upgraded=true
    • cancel_url: /overview/settings/billing
  4. Returns { url } — client redirects.

Inbound webhook — /api/webhooks/stripe

Signature-verified via stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET).

Event Effect
checkout.session.completed plan_status = "pro" for matching stripe_customer_id
customer.subscription.deleted plan_status = "free"
invoice.payment_failed Logs a warning (TODO: dunning email via own API)

Billing portal

Two entry points on /overview/settings/billing:

  • Manage billing in Stripe — standard createBillingPortalSession.
  • Cancel subscription (paid plans only) — createCancelPortalSession opens the portal with flow_data.subscription_cancel pre-focused on the org's active subscription. Falls back to the standard portal if no active subscription is found.

API version

stripe.apiVersion: "2026-03-25.dahlia" — pinned in stripe.ts.