Recipe: Add Stripe Checkout
Recipes
Wire Stripe Checkout, the Customer Portal, and webhooks onto your local stack for subscriptions, billing updates, and feature gates.
45 min
Estimated duration
Stripe
Checkout + Portal
Webhooks
Idempotent handlers
Finish with a paid plan a user can subscribe to via Stripe Checkout, manage from the Customer Portal, and sync back into your DB through webhooks. The apps/api already ships api/billing/ with the right shape. This recipe wires it to a real Stripe account.
Prereqs
Section titled “Prereqs”- A working local stack (
./scripts/compose-up.shclean). - A Stripe account. Test Mode is fine; no business verification needed for this recipe.
- One product + recurring price already created in the Stripe dashboard.
- The Stripe CLI installed locally for webhook forwarding.
-
Flip the feature flag and capture the Stripe secrets in
compose/.env:Terminal window BILLING_ENABLED=trueSTRIPE_SECRET_KEY=sk_test_...STRIPE_WEBHOOK_SECRET=whsec_... # from `stripe listen` (next step)STRIPE_PRICE_ID_FREE=price_... # your free-tier priceSTRIPE_PRICE_ID_PRO=price_... # your paid-tier priceBILLING_ENABLED=falseis the default. The billing routes don’t mount until you flip it. The env validator refuses to boot the API in production if any required Stripe key is missing whileBILLING_ENABLED=true. -
Forward webhooks to your local API:
Terminal window stripe listen --forward-to localhost:3000/api/v1/billing/stripe/webhooksCopy the
whsec_...signing secret printed on first connect intoSTRIPE_WEBHOOK_SECRET. Then restart:Terminal window ./scripts/compose-up.sh # picks up new env -
Extend the UI billing feature.
apps/ui/src/features/billing/already ships the page, queries, and mutations for the starter billing flow. Add or adapt the Checkout call site following the component anatomy. InsideBilling.mutations.ts, POST to the real endpoint via the typed client:Billing.mutations.ts const startCheckout = (planId: string) =>apiClient.POST("/api/v1/billing/stripe/checkout-session", {body: {planId,successUrl: `${window.location.origin}/billing/success`,cancelUrl: `${window.location.origin}/billing`,},});The endpoint returns a Stripe Checkout URL; redirect the browser to it.
-
Surface the Customer Portal. Add a “Manage subscription” mutation that POSTs to
/api/v1/billing/stripe/portal-sessionand redirects the user to Stripe’s hosted portal for cancellation and payment-method updates. -
Add your plan to the ACL feature resolver. The
accountId → activePlan → enabledFeatureschain is the source of truth. Adding aproplan with a feature flag is one entry in the plan config.
Verify
Section titled “Verify”-
Click Upgrade as a logged-in user. The browser redirects to Stripe Checkout. Use Stripe’s test card
4242 4242 4242 4242, any future date, any CVC. -
After payment, the redirect drops you back at the configured
success_url. Thestripe listenterminal showscheckout.session.completedandcustomer.subscription.createdevents arriving. -
In the API’s
audit.audit_logtable you should see entries for the subscription lifecycle (the billing service writes them fire-and-forget). Quick check:SELECT actor, event, payload->>'plan' AS planFROM audit.audit_logWHERE event LIKE 'billing.%'ORDER BY ts DESC LIMIT 5; -
The ACL endpoint
/api/me/featuresreturns the upgraded plan’s features.
What changes in code
Section titled “What changes in code”apps/api/src/api/billing/billing.service.ts: fill in plan-to-price mapping and the webhook handler logic forcheckout.session.completed.apps/api/src/api/billing/billing.routes.ts: already exposes/stripe/checkout-session,/stripe/portal-session,/stripe/webhooksunder the billing router; no new routes needed.- ACL plan config: declare your plan + features so the ACL resolver knows about it.
apps/ui/src/features/billing/: existing feature folder hosting the mutations that call the typed client and the page with the Upgrade button and Customer Portal link.
No new dependencies. No new patterns. Stripe is wired as a pluggable provider so changing pricing later is a config edit, not a code rewrite.
Related
Section titled “Related”- Billing; the existing billing spine and what it ships.
- ACL & feature resolution; plan + feature gating after subscription.
- Audit log; where subscription events get recorded.
- Decision log; why the architecture-as-lint discipline matters for Stripe webhooks.