cmux Billing
Use this skill before changing billing, pricing, Stripe, Pro entitlement, checkout, webhook, or subscription code.
Architecture Map
/api/billing/checkoutcreates Stripe Checkout Sessions for Pro whenSTRIPE_SECRET_KEYis set. It setsclient_reference_idto the Stack user id, auto-creates an anonymous Stack user for signed-out buyers, and falls back to the legacy Stack purchase path when Stripe is unset orplan=team./api/billing/portalresolves the current Stack user, looks up theirstripe_customersrow, and creates a Stripe customer portal session returning to/pricing./api/billing/subscriptioncancels or resumes the current user's active Stripe Pro subscription, and/dashboard/billingrenders localized in-dashboard plan state and self-serve billing actions.web/services/billing/purchase.tsis the shared idempotent recorder used by both/api/billing/completeand/api/stripe/webhook. It attaches email to the purchaser, recordsbilling_email_claimson conflict, and never cross-grants based on an unverified email.cmuxPlanin StackclientReadOnlyMetadatais the only entitlement VM code reads.cmuxVmPlanmanual override wins.resolveProPlanStatusORs legacy Stack products with activestripe_subscriptionsDB rows./api/stripe/webhookis signature-verified, insert-first idempotent throughstripe_webhook_events, safe for foreign events in the shared Stripe account, and gates cmux handling onmetadata.app === "cmux". Return 2xx only after durable writes; return 500 to make Stripe retry.
Dev Workflow
- Use
web/scripts/stripe/dev-stack.sh. - The tagged app bakes
CMUX_PORTintoInfo.plist; run the dev server on the tag's printed port. Do not hardcode a port. - Per-branch Docker Postgres ports can collide with other agents' containers. Use
--db-portand never stop containers you did not create. /app-pricingrequirescmux_app=1.cmux_schemethreads the native deeplink return scheme.cmux-dev-*schemes are honored only for localhost requests.
Repeat Dogfood
- Use a private window for a fresh anonymous buyer.
- Use
web/scripts/stripe/dev-reset.sh <email>to un-Pro a signed-in dev account before retesting checkout. - The "already active" short-circuit lives in
/api/billing/checkout.
Test-Mode Resources
- Product:
prod_UpIQRE6cj0nFjs. - Lookup keys:
cmux-pro-monthly($30/mo) andcmux-pro-yearly($240/yr). - Staging webhook endpoint:
we_1Tq1SZGhInAdn3JbWJReKNENforwarding tocmux-staging.vercel.app; secrets are already in thecmux-stagingVercel project.
Feature Flags
pro-upgrade-ui-enabled-release(PostHog id741838) gates all Pro UI and stays OFF in release until launch. DEBUG builds default the UI on.pro-checkout-enabled-release(PostHog id741839) routes the public pricing CTA.cmux __internal_flags, once merged, inspects and overrides flags locally.
Prod Runbook
- Run
web/scripts/stripe/provision-live.shwith an operator key, add the two Vercel envs, deploy, validate live with a 100 percent-off promotion code purchase, then cancel. - DB migrations go through
bun run cloud-vm:preflight,bun run cloud-vm:migrate -- staging, staging deploy, thenbun run cloud-vm:migrate -- production. See the Cloud VM ops flow. Never run migrations from builds.
Testing Gotchas
bun mock.moduleis process-global, so every module mock must carry every real export other suite files import. Missing exports may surface only in CI's test order asExport named X not found.- Tests must not depend on
DATABASE_URLbeing set. - drizzle-1.0-beta wraps pg errors in
DrizzleQueryError; readerror.causefor the pgcodeandconstraint.
Route Placement Gotcha
Pages outside app/[locale] need a proxy.ts bypass, like /app-pricing and /billing, or next-intl rewrites them into the locale tree and they 404 through missing root layout tags. Those subtrees also need their own layout with html and body.







