TrademarkTrademark
Ctrl k
Search docs...
Slices

Billing

Organization plans on Stripe, with checkout, the portal, feature gates and metered usage.
The billing slice bills organizations, not people. A plan belongs to the tenant: every member gets what it unlocks, and it outlives the admin who bought it. It requires orgs (which brings auth) and adds no dependency: Stripe is called over global fetch and webhook signatures are checked with node:crypto. No surface can decide whether an app takes money, so billing is asked for outright (the api surface is the exception and requires it): Run supabase/billing.sql after init.sql and orgs.sql, and set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.
  • stripe, when STRIPE_SECRET_KEY is set: Stripe Checkout; the webhook mirrors the subscription.
  • mock, when no key, not production: activates the plan in the database directly.
  • off, when no key, NODE_ENV=production: refuses with 503.
Mock mode lets you build and demo checkout, gates and cancelling with Supabase but no Stripe account. It never runs in production, where it would hand plans out for free. src/resources/plans.json is the one place a plan is defined. The billing page renders from it, the server resolves prices and entitlements from it, and stripe:sync provisions Stripe from it. Amounts are in minor units. free is what an organization without a subscription gets. Price ids differ between test and live mode, so they are not in the file: checkout reads STRIPE_PRICE_<KEY> (for example STRIPE_PRICE_PRO_MONTHLY) and falls back to the price's lookup_key. A stack.json with answers.billing seeds this file at scaffold time. The scaffolder adds the stripe:sync script to a generated project's package.json; in the template itself, run node scripts/stripe-sync.mjs. It creates the products, prices and meters, prints the STRIPE_PRICE_* lines for .env.local, and lists what only the Stripe dashboard can do: the webhook endpoint and its secret, and the customer portal. Re-running is safe and is how a price changes. A changed amount creates a new price under the same lookup_key and archives the old one; existing subscriptions stay on what they were sold. The org guard answers first, so a non-member still gets a 404. A member whose plan lacks the feature gets 402. An optional third argument sets the minimum role. The template gates creating invitations (invitations) and the analytics page (analytics, which shows an upgrade notice); without the slice the scaffolder removes both gates. For more than yes or no, getOrgBilling(guard) returns the plan, the subscription and whether the caller may manage it. Only active and trialing entitle. A past_due subscription stops access at the failed renewal, and the billing page shows why. billing_subscriptions mirrors live subscriptions so a gate is one read. Point Stripe at /api/webhooks/stripe with checkout.session.completed, customer.subscription.created, .updated, .deleted, invoice.paid and invoice.payment_failed. Each event re-fetches the subscription from Stripe rather than trusting the payload, so retries and out-of-order delivery converge. Ended subscriptions are deleted from the mirror, and an organization with a live subscription cannot be deleted. A price with a meter bills per unit. pro-api in the shipped catalog includes 5,000 units a month, then charges 0.3 cents each: Use amount instead of tiers for a flat rate. Checkout adds a plan's metered prices for the chosen interval to the same subscription, so the fee and its usage share one invoice. Report usage with reportUsage(guard, { event, value, identifier }), with a unique identifier so a retry is not billed twice. Jobs with no signed-in user use reportUsageForOrganization(orgId, usage) and getBillingForOrganization(orgId); pass them only ids your own server produced. In mock mode reporting succeeds without sending anything. API products is built on this.
  • GET /api/orgs/[org]/billing — requireOrg: any member sees the plan.
  • POST /api/orgs/[org]/billing/checkout — requireOrgAdmin, refused while a plan is active.
  • POST /api/orgs/[org]/billing/portal — requireOrgAdmin; in mock mode, cancels.
  • POST /api/webhooks/stripe — Stripe's signature.
The page is /app/[org]/billing, and it checks membership itself.
Last updated October 18, 2026
Edit this page on GitHub