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
- User clicks Upgrade →
POST /api/internal/billing. - Server finds or creates a Stripe customer, persists
stripe_customer_idon the org. - Creates Checkout Session with
STRIPE_PRO_PRICE_IDand:success_url:/overview/settings/billing?upgraded=truecancel_url:/overview/settings/billing
- 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) —
createCancelPortalSessionopens the portal withflow_data.subscription_cancelpre-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.