billing: App Store Go hours, Pro/Max compute allowances, Sandbox allowlist
Summary
Server work so a cmux Go, Pro, or Max subscription bought on iPhone works against production and stays economical.
- Go hours for App Store buyers.
getGoVmUsageread only web subscriptions, and the database capacity guardcmux_guard_go_vm_capacityread onlystripe_subscriptions, so every App Store Go subscriber got 0 hours andcmux_go_period_unavailable. A sharedpersonalBillingPeriod(newweb/services/vms/planUsage.ts) now returns the plan and period from the web subscription or the App Store subscription (newest transaction's purchase date to expiry, extended by a grace period), higher plan first. Migration20261014160000_go_runtime_limits_applereplaces the guard function so it falls back toapple_subscriptions. Only the period lookup changes; it adds no table or column. - Pro and Max allowances: code only, OFF by default. https://cmux.com/pricing promises no hour limits, so the Pro/Max compute-hour and disk allowances run only with
CMUX_VM_PLAN_BUDGETS=on. With the default, no Pro or Max user gets a compute-hour or disk limit, the 402vm_compute_budget_reached/vm_storage_budget_reachedpaths never fire, the minute job reads only Go machines, andGET /api/vmadds no budget fields. Go's 40 VM-hours (existing behavior) stay enforced. When the switch is on, the allowance is metered on the existing runtime meter (cloud_vm_runtime_intervals) at each machine's recorded shape, enforced at create, fork, restore, resume, and disk growth, and a yearly subscription resets monthly. The numbers below are the proposal for that switch. - Sandbox entitlement. In production a Sandbox purchase grants a plan only for
com.cmux.appand only when the signed-in account's primary email is verified and inAPPLE_IAP_SANDBOX_ACCOUNT_EMAILS. Any other TestFlight tester getspurchases_unavailablefrom/api/billing/apple/account-token, so the app shows no buy buttons. The grant check reads the email only when a Sandbox row would otherwise grant.APPLE_IAP_SANDBOX_ENTITLEMENTSkeeps its override semantics. - Analytics.
subscription_entitlement_grantedis sent from both the app-posted transaction and the notification, after the write is applied and only when that subscription is the user's granting plan (insert IDapple_entitlement:<transaction id>). It and the Applesubscription_*events carrybilling_period(monthly|yearly). - Yearly App Store products.
com.cmux.app.pro.yearlyandcom.cmux.app.max.yearly(and the beta-bundle equivalents) map to Pro and Max, matching the web yearly prices ($480/yr, $1,920/yr). Go has no yearly web price, so it stays monthly only. The account-tokenproductsarray has one entry per (plan, period), for example{"productId":"com.cmux.app.pro.yearly","planId":"pro","billingPeriod":"yearly"}. Yearly entries are sent only when the app sendsx-cmux-billing-periods: monthly,yearly; otherwise the response has monthly entries only, as today, each withbillingPeriod: "monthly". Transactions and notifications map either ID to its plan, and the grant runs untilexpires_at. A non-USD yearly charge is estimated at the monthly list price x 9.6 until the App Store yearly prices are set. - Vendor names in user copy.
dashboard.billing.banners.nosub,guestCLI.routeToken,docs.cloudSecurity.isolationVm, anddocs.cloudSecurity.credentialsModelno longer name vendors (en, ja; the other locales have no copy of these keys).
Budget math
The machine provider's list rates are $0.04032 per vCPU-hour, $0.0129 per GiB-hour of memory, and $0.063 per GiB-month of disk. One compute-hour is 1 vCPU + 2 GiB: $0.04032 + 2 x $0.0129 = $0.06612. The worst case is the whole allowance used and the whole disk cap kept for the month. The target is under 70% of web net revenue ($50 Pro, $200 Max). App Store net is higher ($52.49 / $209.99 in year one).
| Plan | Compute | Disk | Worst case | % of web net | % of iOS net |
|---|---|---|---|---|---|
| Pro | 375 compute-hours = $24.80 | 160 GB = $10.08 | $34.88 | 69.8% | 66.4% |
| Max | 1,500 compute-hours = $99.18 | 640 GB = $40.32 | $139.50 | 69.8% | 66.4% |
The default 4 vCPU / 8 GB machine draws 4 units an hour. Pro is about 94 default-machine hours a month and Max about 375. 160 GB is 5 default 32 GB disks, so Pro can still keep its 5 machines. Without the allowance, one user running the whole pool 24/7 costs about $1,005 (Pro) and $3,942 (Max) a month. For comparison, Go is 40 h x 2 units = 80 compute-hours ($5.29) plus 2 x 16 GB of disk ($2.02), so $7.31 or 73% of $10.
Not counted: persistent home volumes (persistentHome) and any paused-memory storage the provider bills. Payment processing fees are not subtracted from net revenue.
Rollout
- Migration
20261014160000_go_runtime_limits_appleis applied to PlanetScalecmux-prodstagingand then productionmain(2026-10-14, from commit 92c0697555). The same run also recorded20261014120000_cloud_vm_health_snapshots(already on main, additive) and re-recorded20261013120000_hexclave_webhook_inboxand20261013130000_stack_session_revocations, whose earlier ledger rows had no name (both areIF NOT EXISTSonly). - Set production
APPLE_IAP_SANDBOX_ACCOUNT_EMAILS=cmux-rev-0e6ba145@mailinator.com,lawrence@manaflow.aibefore this deploys. Without it, App Review sees no products. Only verified emails match; email-code sign-in verifies the review account. - Leave
CMUX_VM_PLAN_BUDGETSunset (allowances off). - Create the yearly products in App Store Connect (
com.cmux.app.pro.yearly,com.cmux.app.max.yearly) in the same subscription group.
Why Apple subscription_* events were zero
- Production has 0
apple_subscriptionsrows, 0apple_transactionsrows, and 144 minted account tokens. No purchase ever reached the server, so no event could fire. apple_notificationshas 1 row: a SandboxTESTnotification on 2026-10-02. The Production notification URL could not be confirmed with theascCLI because it does not returnsubscriptionStatusUrl.POST https://cmux.com/api/billing/apple/notificationsanswers 400invalid_notificationto a bad payload, so the route is reachable.https://www.cmux.com/...answers 301, which App Store notifications do not follow.- Events are not filtered by StoreKit environment. They are sent only when
VERCEL_ENV=production, so staging (beta-bundle Sandbox notifications) sends none. - Code cause, fixed here: lifecycle events came only from notifications, so a purchase recorded through the app post sent nothing.
subscription_entitlement_grantednow covers both paths.
Testing
- New DB behavior test
web/tests/plan-allowance-db-behavior.test.ts(real Postgres with every migration applied): an App Store Go subscriber starts a Go machine and is metered on its own period; Pro meters compute units and saved disk on the web period; an App Store Max outranks a web Pro; Founder's Edition has no allowance. Red on the first commit (getGoVmUsagereturned the exhausted placeholder period for an App Store Go subscriber; the Pro/Max module did not exist), green on the fix: 13 pass withapple-iap-db-behavior. web/tests/fixtures/go-runtime-limits.sqlgains an App Store Go block for the guard.- Ran on a Freestyle VM with Postgres and every migration applied: the guard fixture (both blocks PASS);
vm-workflows.test.tswithCMUX_DB_TEST=1(185 pass);apple-iap-service,apple-iap-account-token,apple-iap-routes,go-pause,go-runtime-limits,dashboard-billing-screen,vm-route-auth,billing-plan-route(each file isolated, all pass);tsgo --noEmit(no errors inweb/; only the two test files that importworkers/, which was not synced, failed). Not run locally: the complexity gate (needs git) and the full suite; CI covers them. - Existing tests updated for the new Sandbox policy (
apple-iap-service,apple-iap-account-token) and copy (dashboard-billing-screen).
Changelog
- Fixed: an App Store Go subscription now includes its 40 VM-hours.
- Added: yearly Pro and Max subscriptions in the iOS app.
Summary by CodeRabbit
- New Features
- Pro and Max cloud VM plans support monthly compute-hour and storage allowances. When enabled, limits may block creating, resuming, or resizing VMs, and running VMs may pause when compute time runs out.
- VM usage details show applicable budgets, usage, and reset timing. Localized messages explain limits and suggest cleanup or upgrade options.
- App Store subscriptions support yearly billing for Pro and Max. App Store billing periods can also inform VM usage limits and entitlement-granted analytics.
- Go VM usage periods can be based on eligible App Store subscriptions as well as web subscriptions.
- Bug Fixes
- Production Sandbox purchases for the App Store app require a verified primary email on the configured allowlist, unless an entitlement override applies.
- Billing notices now use web-subscription wording rather than referring only to Stripe.