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.

infrastructureworkflow

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.

So what Worth a look if you need Stripe subscriptions on a Supabase + Express stack. The migration is idempotent and RLS-aware, so lifting it wholesale is reasonable. One dependency to check: token attribution in `tokens_used_this_period` relies on the AI Gateway migration that preceded this. Pull that first or wire your own counter.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from hosman20/mike-2.0, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
ba00445b feat(billing): add Stripe subscriptions and paywall middleware z 2026-05-13 ↗ GitHub
commit body
Batch 5 - gate AI chat endpoints behind an active subscription
(or active 14-day trial) and record token usage against the
subscription's monthly limit.

Database:
- backend/migrations/001_add_subscriptions.sql: idempotent
  (CREATE IF NOT EXISTS), RLS-enabled, auto-provisions a trial
  row on every new auth.users insert via trigger.
- backend/schema.sql: mirror of the migration appended.

Backend:
- backend/src/lib/stripe.ts: Stripe SDK wiring.
- backend/src/lib/billing/usage.ts: recordTokenUsage() fire-and-
  forget bookkeeping after stream completion (never blocks the
  hot path, never throws).
- backend/src/routes/billing.ts: checkout/portal/webhook endpoints.
  Handles checkout.session.completed and
  customer.subscription.{created,updated,deleted}.
- backend/src/middleware/requireActiveSubscription.ts: paywall.
- backend/src/index.ts: mount raw-body parser ahead of express.json
  for the Stripe webhook signature check; gate POST /chat,
  /projects/:id/chat, /tabular-review/:id/chat, and
  /tabular-review/:id/generate with requireAuth + paywall.
- backend/.env.example: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
  and STRIPE_PRICE_{STARTER,PROFESSIONAL,ENTERPRISE}.
- backend/package.json: stripe@^22.1.1 + vitest test runner.
- routes/{chat,projectChat,tabular}.ts: void recordTokenUsage(...)
  after each successful stream.

Frontend:
- frontend/src/lib/billing.ts: tier metadata + helpers.
- frontend/src/app/lib/mikeApi.ts: HTTP 402 interceptor redirects
  the browser to /pricing?reason=... on paywall rejection.

Tests:
- 6 middleware tests + 4 webhook tests (10 new tests total).

Agent ID: aa83ddc67d4dfac03 (general-purpose).
d3956d04 feat(billing-ui): pricing page, billing settings, and trial banner z 2026-05-13 ↗ GitHub
commit body
Batch 6 - surface the new subscription state in the product:
public pricing page, in-app billing settings tab, and a top-of-
main trial countdown banner.

- frontend/src/app/pricing/page.tsx: public /pricing route, lands
  logged-out users redirected from a 402. Enterprise CTA stubs out
  to mailto:sales@mike.ai (placeholder).
- frontend/src/app/(pages)/account/billing/page.tsx: in-app billing
  settings - current plan, trial status, token usage, manage button
  (Stripe customer portal).
- frontend/src/components/chrome/trial-banner.tsx: countdown banner
  for trialing users, mounted at top of <main> in (pages)/layout.tsx.
- frontend/src/app/(pages)/account/layout.tsx: re-add the "Billing"
  tab (replaces the "Models & API Keys" tab removed in batch 4).
- frontend/src/app/lib/mikeApi.ts: SubscriptionTier/Status/Info
  types + subscription field on UserProfile.
- frontend/src/contexts/UserProfileContext.tsx: thread subscription
  through the profile context.
- backend/src/routes/user.ts: return the subscription block on the
  /user/profile response.
- 5 new tests (3 pricing + 2 billing).

Agent ID: ad65cb0ceb7e70da6 (general-purpose).

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-409.md from inside the repo you want the changes in.

⬇ Download capture-thread-409.md