Repository navigation
Remove legacy Stack billing entirely (checkout + recognition) - #7662
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR removes the legacy Stack-hosted checkout/confirm billing flow and the "external" billing-management state entirely, replacing it with Stripe-only checkout, portal, and plan resolution. The checkout route gates on Stripe configuration, the confirm route is deleted, ChangesStripe-only billing migration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (23 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR rips out cmux's legacy Stack Auth-hosted billing path entirely — both the hosted checkout (
Confidence Score: 5/5Safe to merge — the cut is clean, all legacy paths are removed consistently across routes, plan resolution, UI, and all locale files. The billing source of truth is now a single DB query against stripe_subscriptions with no fallback heuristics. The confirm-route polling loop (which was genuinely hacky) is gone. billingAvailable is now correctly derived rather than hardcoded. 548 tests cover the behavioral changes and no test regressions are reported. The only dead code left is the welcome=pending branch in ProWelcomeBanner, which is unreachable and harmless. No files require special attention. The welcome=pending handling in pro-welcome-banner.tsx is unreachable dead code but does not affect correctness. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GET /api/billing/checkout] --> B{Stripe configured?}
B -- No --> C[Redirect: /pricing?billing=unavailable]
B -- Yes --> D{plan=pro or team?}
D -- pro --> E[stripeProCheckout]
D -- team --> F[stripeTeamCheckout]
D -- other --> G[Redirect: /pricing?billing=invalid_plan]
E --> H[Stripe Checkout Session]
F --> H
H --> I[GET /api/billing/complete webhook]
I --> J[stripe_subscriptions row created]
K[resolveProPlanStatus] --> L{user.id present?}
L -- Yes --> M[hasActiveStripeProSubscription\nquery stripe_subscriptions]
L -- No --> N[isPro = false]
M -- active row --> O[isPro=true, billingManagement=stripe]
M -- no row --> P[isPro=false, billingManagement=none]
Q[GET /api/billing/portal] --> R{Stripe customer row?}
R -- Yes --> S[Stripe Billing Portal]
R -- No --> T[Redirect: /pricing?billing=unavailable]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[GET /api/billing/checkout] --> B{Stripe configured?}
B -- No --> C[Redirect: /pricing?billing=unavailable]
B -- Yes --> D{plan=pro or team?}
D -- pro --> E[stripeProCheckout]
D -- team --> F[stripeTeamCheckout]
D -- other --> G[Redirect: /pricing?billing=invalid_plan]
E --> H[Stripe Checkout Session]
F --> H
H --> I[GET /api/billing/complete webhook]
I --> J[stripe_subscriptions row created]
K[resolveProPlanStatus] --> L{user.id present?}
L -- Yes --> M[hasActiveStripeProSubscription\nquery stripe_subscriptions]
L -- No --> N[isPro = false]
M -- active row --> O[isPro=true, billingManagement=stripe]
M -- no row --> P[isPro=false, billingManagement=none]
Q[GET /api/billing/portal] --> R{Stripe customer row?}
R -- Yes --> S[Stripe Billing Portal]
R -- No --> T[Redirect: /pricing?billing=unavailable]
Reviews (3): Last reviewed commit: "billing: restore Stripe pending banner s..." | Re-trigger Greptile |
cmux migrated from Stack Auth-hosted subscription products to direct Stripe billing. This rips out the legacy path completely: - Delete legacyStackCheckout and the /api/billing/confirm return route. Checkout with Stripe unconfigured now redirects to /pricing?billing=unavailable instead of the Stack hosted flow. - resolveProPlanStatus: Pro is now true iff there is an active row in stripe_subscriptions (user or team scope). Stack product subscriptions (customer.listProducts) no longer grant Pro; the VM entitlement check follows the same rule. - BillingManagementKind drops "external"; the "managed by our previous billing system, contact support" message and its keys are removed from services, the plan API, every billing UI, and all locale catalogs (en/ja/km/th). Existing legacy-only subscribers become Free (intentional; no migration). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c66cd47 to
4eff884
Compare
The legacy-billing rebase over #7645 dropped the App Store distribution gate that hid the Stripe /api/billing/portal link for Pro users (Apple 3.1.1 bans external purchase links in App Store builds). Restore the !appStorePaymentGated gate and cover the Pro + App Store combination, which no test exercised.
| return { planId: FREE_PLAN_ID, billingManagement: "none" }; | ||
| } | ||
| const stripeActive = await hasActiveTeamSubscriptionForTeam(team.id); | ||
| const metadataActive = metadataPlanId(team.clientReadOnlyMetadata) === TEAM_PLAN_ID; |
There was a problem hiding this comment.
Team VM metadata entitlement drift
Medium Severity
Team plan status now depends only on an active stripe_subscriptions row, but Cloud VM billing still treats a team’s clientReadOnlyMetadata.cmuxPlan as authoritative. Legacy Stack-only teams can show as Free in billing APIs and UI while VM create still resolves paid team limits from stale metadata.
Reviewed by Cursor Bugbot for commit 526da14. Configure here.
… Stripe config Two regressions from removing legacy Stack billing: - /api/billing/complete (async payment) and /billing/success (subscription not yet active due to webhook race) still redirect to /pricing?welcome=pending, but the pending branch was dropped from ProWelcomeBanner, leaving paid users on a blank page. Restore the welcomePending message + a Check again link that reloads /pricing to re-check status (the deleted /api/billing/confirm route is not resurrected). Re-add welcomePending/welcomePendingAction to en + ja. - Checkout now requires Stripe, but /api/billing/plan still reported billingAvailable:true whenever Stack was configured, so Stripe-less environments advertised an upgrade flow that dead-ends at billing=unavailable. Derive billingAvailable from isStripeBillingConfigured() for both the anonymous and authenticated responses.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6cfb25f. Configure here.
| }; | ||
|
|
||
| export type ActiveStripeSubscriptionQuery = (stackUserId: string) => Promise<boolean>; | ||
| export type BillingManagementKind = "stripe" | "external" | "none"; |
There was a problem hiding this comment.
Stale metadata keeps VM Pro access
High Severity
Pro is now determined only from active stripe_subscriptions rows, but Cloud VM limits still read cmuxPlan / cmuxVmPlan from Stack metadata on most requests. Legacy-only subscribers can keep paid VM entitlements until metadata is cleared by a billing reconcile or plan resolution call.
Reviewed by Cursor Bugbot for commit 6cfb25f. Configure here.
…O_PRODUCT_ID (#7808) PR #7757 (account.me) and #7662 (legacy Stack billing removal) were green independently but conflict once both merged: #7662 deleted PRO_PRODUCT_ID and made resolveProPlanStatus Stripe-only (no more Stack listProducts), while #7757's account-me-orpc.test.ts imported PRO_PRODUCT_ID and drove Pro via a fake Stack product list. The squash-merge didn't re-run CI against the combined tree, so main's web-typecheck/tests went red. Rewrite the test for the post-#7662 world: no PRO_PRODUCT_ID, no Stack-product fake. It drives the real (Stripe-only) resolveProPlanStatus with an id-less user so the subscription lookup short-circuits to false with no database, and asserts account.me maps the Free plan + email onto its response, plus the auth gate and the OpenAPI spec/byte-identity checks. typecheck clean; full web suite green.


What
Rip out cmux's legacy Stack Auth-hosted billing path completely — both the hosted checkout AND recognition of legacy Stack subscriptions. After this, a user/team is Pro if and only if they have an active row in the
stripe_subscriptionstable (direct Stripe billing).Motivation: the legacy path was already dead in production (both pro and team use direct Stripe when
STRIPE_SECRET_KEYis set), but it still surfaced the dead-end "Your subscription is managed by our previous billing system. Contact support…" state for anyone whose subscription lived in Stack's product system.Changes
legacyStackCheckoutand the/api/billing/confirmroute.GET /api/billing/checkoutwith Stripe unconfigured now redirects to/pricing?billing=unavailable(no Stack fallback).resolveProPlanStatus: Pro ⇔ activestripe_subscriptionsrow (user or team scope). RemovedhasActiveProSubscription/customer.listProductsrecognition. The VM entitlement check inapp/api/vm/route.tsfollows the same rule.BillingManagementKinddrops"external"→"stripe" | "none". The "previous billing system / contact support" message and its keys are removed from services, the plan API, every billing UI (pricing, app-pricing, dashboard billing, pro-welcome-banner, portal), and all four locale catalogs (en/ja/km/th).services/billing/purchase.ts.Existing legacy-only subscribers become Free — intentional, per product decision; no migration/backfill.
Preserved
Direct-Stripe billing end to end: pro + team checkout, portal,
/api/billing/complete, webhooks, and thestripe_subscriptionsreconcile are untouched except for removing legacy references.Verified
bun run typecheckclean; full sorted web suite 548 tests, 0 fail (tests assert the new rules behaviorally: a Stack-metadata-only Pro snapshot renders as Free with the upgrade CTA; astripe_subscriptionsrow renders as Pro with Manage-billing; unconfigured checkout →/pricing?billing=unavailable).legacyStackCheckout,/api/billing/confirm,hasActiveProSubscription,billingExternal, or"external"billingManagement.billingExternalremoved from all four message catalogs, keys in sync.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
High Risk
This intentionally revokes Pro for legacy Stack-only subscribers with no data migration, and tightens entitlement checks used at VM create and TestFlight—misconfigured Stripe env can block checkout and downgrade perceived access.
Overview
Removes the legacy Stack Auth billing path end-to-end — hosted checkout, post-purchase confirm polling, and any recognition of Stack
listProductsgrants.Checkout requires Stripe:
GET /api/billing/checkoutno longer falls back to Stack; without Stripe config it redirects to/pricing?billing=unavailable. The/api/billing/confirmroute is deleted, and pending-activation UI no longer links there (e.g. pending banner points at/pricing).Pro and Team status are Stripe-only:
resolveProPlanStatus, team plan API, VM create reconcile, and TestFlight eligibility (isTestflightEligible) treat an account as paid only when there is an active row instripe_subscriptions(metadata may still sync via reconcile).BillingManagementKinddropsexternal; users without a Stripe customer get portal redirects tobilling=unavailableinstead of “previous billing system” copy (removed from UI and locales).Product impact: accounts that were Pro only via Stack products or stale
cmuxPlanmetadata show as Free and see upgrade CTAs; dashboard billing drops the legacy plan section in favor of Free when there is no Stripe subscription row.Reviewed by Cursor Bugbot for commit 6cfb25f. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Removed legacy Stack billing so Stripe is the only billing path for Pro and Team. Legacy Stack-only subscribers now render as Free.
Refactors
legacyStackCheckoutand/api/billing/confirm;/api/billing/checkoutnow requires Stripe and otherwise redirects to/pricing?billing=unavailable. Plan resolution is Stripe-only;BillingManagementKindis"stripe" | "none". VM entitlements and TestFlight follow the same rule.billing=unavailable. App Store builds hide the Pro billing-portal link. Ensure Stripe is configured (STRIPE_SECRET_KEYand price IDs) in all environments.Bug Fixes
/pricing(no confirm route)./api/billing/plannow reportsbillingAvailablebased onisStripeBillingConfigured().Written for commit 6cfb25f. Summary will update on new commits.
Summary by CodeRabbit