SKILL.md
cmux Billing
Read before changing billing, pricing, Stripe, Pro entitlement, checkout, webhook, or subscription code.
Architecture map
/api/billing/checkoutcreates Stripe Checkout Sessions for Pro whenSTRIPESECRETKEYis set. It setsclientreferenceidto 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. The "already active" short-circuit lives here./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 active Stripe Pro subscription;/dashboard/billingrenders localized in-dashboard plan state and self-serve actions.web/services/billing/purchase.tsis the shared idempotent recorder used by/api/billing/completeand/api/stripe/webhook. It attaches email to the purchaser, recordsbillingemailclaimson conflict, and never cross-grants based on an unverified email.- VM code reads the plan from Stack
clientReadOnlyMetadata: a non-emptycmuxVmPlan(operator override) takes precedence, otherwisecmuxPlan(the Stripe mirror) supplies the entitlement.resolveProPlanStatusreports Pro for an activestripe_subscriptionsrow or a paidcmuxVmPlanoverride (pro,team,founders);billingManagementstays Stripe-only, so a granted account shows Pro without a portal link. /dashboard/admin(nav groupadmin) lets verifiedcmux.com,manaflow.ai, andmanaflow.comaccounts search users, teams, and emails and, after a confirmation dialog: grant or remove the usercmuxVmPlanoverride (/api/admin/users), grant or remove a team override ofteam(/api/admin/teams), grant Pro to an email with no account yet (/api/admin/email-grants, stored inadminplangrantsand applied by the after-sign-in callback once the mailbox is verified), and downgrade a paying customer by cancelling the Stripe subscription at period end (/api/admin/subscriptions, same service as the self-serve form).services/admin/access.tsis the gate,services/admin/proGrants.tswrites under the account-mutation lease and recordsserverMetadata.cmuxAdminPlanGrant(who, when, which plan). Non-admins get 404 on the page and 403 on the API. Theadminplangrantsmigration is an operator step; until it runs, pending email grants report 503 and search omits them./api/stripe/webhookis signature-verified, insert-first idempotent throughstripewebhookevents, 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. - Local
bun devaccounts use the development Stack project and receive a
non-persistent Pro entitlement automatically. This applies only when CMUXLOCALDEVPRO=1, NODEENV=development, VERCELENV is unset, and NEXTPUBLICSTACKPROJECT_ID is the development project. Release and preview deployments remain billing-backed. Use dev-grant.sh only when testing an explicit manual grant or a non-local deployment.
- The tagged app bakes
CMUX_PORTintoInfo.plist; run the dev server on the tag's printed port, never a hardcoded one. - Per-branch Docker Postgres ports collide with other agents' containers. Use
--db-portand never stop containers you did not create. /app-pricingrequirescmuxapp=1.cmuxschemethreads 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, and
web/scripts/stripe/dev-reset.sh <email>to un-Pro a signed-in dev account before retesting checkout. - Fake payment, two ways:
web/scripts/stripe/dev-grant.sh <email>writescmuxVmPlan: "pro"directly (same override as the admin page) (instant, no checkout; undo with dev-reset). For the full checkout path at $0, enter promotion codeCMUXDEV100in test-mode checkout — a 100%-off forever coupon in the test account;allowpromotioncodesis already set on checkout sessions. - Newer Stripe CLI prints
stripe config --listaskey=value(older builds usedkey = 'value'); dev-stack.sh and dev-reset.sh accept both. If key extraction fails, re-runstripe login.
Catalog
Prices live in web/services/billing/plans.ts and are provisioned by web/scripts/stripe/provision-catalog.sh. Current checkout keys: cmux-pro-monthly-50 ($50/mo), cmux-pro-yearly-480 ($480/yr, $40/mo equivalent), cmux-team-monthly-60 ($60/user/mo), cmux-team-yearly-576 ($576/user/yr, $48/mo equivalent). Stripe amounts are immutable, so a price change mints a new lookup key carrying the amount and adds the old key to LEGACYPRICELOOKUPKEYS; grandfathered Prices (cmux-pro-monthly $30, cmux-pro-yearly $240, cmux-pro-yearly-288 $288, cmux-team-monthly $35, cmux-team-yearly-336 $336) stay active for the subscriptions on them and /dashboard/billing prices every row from its own Stripe amount. Env price-id overrides carry the amount in their name (STRIPEPROMONTHLY50PRICEID and friends); a retired name fails env validation, so delete it from Vercel before deploying a price change. Test-mode Pro product prodUyHgRPpmCzrkLJ, live prodUq4a28vk0fP3E6. Staging webhook endpoint we_1Tq1SZGhInAdn3JbWJReKNEN forwards to cmux-staging.vercel.app; its secrets are already in the cmux-staging Vercel project.
The paid Cloud VM allowance lives in web/services/vms/machineSpec.ts: Pro allows up to 50 machines for one paid account. Team grants 50 machines per paid seat. Each machine has its own CPU, memory, and disk; there is no aggregate resource pool. The default machine has 8 GB RAM and 32 GB disk. Disk grows independently up to 256 GB. The repository enforces machine counts under the billing-team lock and tracks individual shapes for fork, snapshot, and resize recovery. Paid-limit environment overrides are retired; the create kill switch remains the incident control.
Feature flags
pro-upgrade-ui-enabled-release (PostHog id 741838) gates all Pro UI and stays OFF in release until launch; DEBUG builds default it on. Public Pro and Team pricing CTAs always route through /api/billing/checkout, never the download confirmation page. cmux __internal_flags, once merged, inspects and overrides flags locally.
Prod runbook
Run web/scripts/stripe/provision-live.sh with an operator key, add the two Vercel envs, deploy, validate live with a 100-percent-off promotion code purchase, then cancel.
DB migrations: bun run cloud-vm:preflight, bun run cloud-vm:migrate -- staging, staging deploy, then bun run cloud-vm:migrate -- production. Never run migrations from builds. See the Cloud VM ops flow.
Gotchas
bun mock.moduleis process-global, so every module mock must carry every real export other suite files import. A missing export can 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. - Pages outside
app/[locale]need aproxy.tsbypass (like/app-pricingand/billing), ornext-intlrewrites them into the locale tree and they 404 through missing root layout tags. Those subtrees also need their own layout withhtmlandbody.