Billing
Stripe subscriptions, trials, customer portal, and webhooks.
Billing integrates Stripe for subscriptions: a plan
picker, checkout, the hosted customer portal, per-seat quantity sync, and
webhook processing. The local Subscription row is always a cached
projection of Stripe — written only by webhook processing, never the source of
truth. Plans are declared in config/plans.ts, which also seeds the marketing
pricing page.
Optional feature
Billing is optional (core: false, requires: ["orgs", "email", "jobs"]).
It owns the plan side of the entitlement seam that flags composes.
What it does
- Plans —
config/plans.tsdeclares plan keys, prices, and entitlements;PlanPickerrenders them (reused by the marketing pricing page). - Checkout & portal — Stripe Checkout for new subscriptions and the hosted
billing portal for changes, from
BillingSettings. - Webhooks —
/api/webhooks/stripeverifies the signature, records the event in an idempotency ledger, and enqueues a job that upserts theSubscriptionprojection. - Seat sync —
enqueueSeatsSync(orgId)(called by orgs on membership change) reconciles the Stripe per-seat quantity to the live member count, best-effort and idempotent. - Metered usage push —
reportOrgMeterUsageis the Stripe Meters seam the usage feature reports rolled-up totals through. - Plan resolution —
getPlanEntitlementsmaps a subscription (incl. an adminplanKeyOverride) to plan-default entitlements; flags overlays per-org overrides on top.
Models it adds
| Model | Purpose |
|---|---|
Subscription | Cached projection of an org's Stripe subscription (plan, status, trial, seat quantity). One row per org; Free orgs have none. |
StripeEvent | Webhook idempotency ledger keyed by Stripe event id — a replayed event is skipped exactly once. |
Adds the marker-wrapped Organization.subscription relation.
Env it needs
STRIPE_SECRET_KEY= # server-side Stripe API key
STRIPE_WEBHOOK_SECRET= # signing secret for /api/webhooks/stripe
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= # client-side publishable keyAdds the stripe:sync script (push local plans to Stripe) and the stripe
compose service (webhook forwarding in dev).
Composition
requires: ["orgs", "email", "jobs"]— subscriptions are org-scoped, receipts are emailed, and webhook/seat work runs as jobs (billing/stripe-event,billing/seats.sync).- Dependents —
flagsrequires: ["billing"](and through flags,usageandadmin);marketingrequires: ["billing"]for the pricing page. - Exposes
getSubscriptionSummary/setPlanKeyOverridefor the admin panel.
What pruning removes
Leaving billing out drops features/billing, config/plans.ts, the
/api/webhooks/stripe route, the billing settings page, the Subscription and
StripeEvent models, the Organization.subscription relation, and the billing
jobs — plus the stripe compose service and the stripe:sync script. Because
flags requires billing, deselecting billing also removes flags, and
transitively usage, admin, and marketing.