Stripe subscriptions, paywall middleware, and billing UI wired end-to-end
Two commits add the full monetization layer: a Supabase subscriptions table with auto-provisioned 14-day trials, HTTP 402 paywall middleware on all chat endpoints, and a pricing page plus billing settings tab on the frontend. This is the feature the prior gateway migration was building toward.
The backend commit (ba00445) does the database work first. backend/migrations/001_add_subscriptions.sql creates an RLS-enabled subscriptions table and a Postgres trigger (on_auth_user_created_subscription) that fires on every auth.users insert, provisioning a trialing row with a 14-day window and a 1M-token monthly limit. The trigger swallows exceptions so a Stripe misconfiguration never blocks signup. RLS policy restricts users to their own row; the service role bypasses it for webhook writes.
backend/src/middleware/requireActiveSubscription.ts returns HTTP 402 with a typed PaywallReason enum: no_subscription, trial_expired, subscription_inactive, token_limit_reached. The paywall is mounted on POST /chat, /projects/:id/chat, /tabular-review/:id/chat, and /tabular-review/:id/generate. Token recording is fire-and-forget via recordTokenUsage(), explicitly designed to never block or throw on the hot path. Checkout, portal, and webhook routes (checkout.session.completed, customer.subscription.{created,updated,deleted}) live in backend/src/routes/billing.ts. Tier price IDs come from STRIPE_PRICE_{STARTER,PROFESSIONAL,ENTERPRISE} env vars. A raw-body parser sits ahead of express.json for webhook signature verification. Ten new tests.
The frontend commit (d3956d0) adds the public /pricing page, an in-app /account/billing tab, and components/chrome/trial-banner.tsx - a countdown banner mounted in (pages)/layout.tsx. A 402 interceptor in mikeApi.ts redirects to /pricing?reason=.... UserProfileContext now threads a subscription block through the profile, fed from an updated /user/profile backend route. Enterprise is a mailto stub.
The structured 402 payload - four reason codes, no free-form messaging - is the cleanest thing here. If your product needs paywalled AI endpoints, cribbing that contract alone is low-effort.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?