Mike 2.1 Phase 1: design system, Vercel AI Gateway, Stripe paywall

✅ merged · #1 · hosman20/mike-2.0 ← hosman20/mike-2.0 · opened 13d ago by hosman20 · merged 12d ago by hosman20 · self · +8,621-2,748 across 72 files · ↗ on GitHub

From the PR description

Summary

Phase 1 of the Mike 2.1 rebrand and commercial-SaaS transition. Forks the original mike-2.0 prototype into a paid product targeting Middle East law firms.

Twelve commits, one per logical batch, each independently revertable. Full audit trail in docs/mike-2.1-run-log.md.

What changed

Design (Mike 2.1 "warm paper + ink" system)

  • New Tailwind v4 @theme block in globals.css - near-black accent (#0A0A0A), hairline #E5E7EB borders, no shadows
  • 4 shadcn primitives rebuilt as ink pills: Button, Input, Badge, DropdownMenu
  • Login + signup pages migrated; signup now requires firm name (B2B onboarding)
  • App shell split into 64px IconRail + 224px SecondaryNav, Playbooks nav added
  • not-found.tsx migrated off legacy font-eb-garamond / gray scale

AI provider (Vercel AI Gateway)

  • All direct provider SDK calls (Anthropic, Gemini, OpenAI) replaced with Vercel AI Gateway via AI SDK v6
  • User-facing API key UI (/account/models) deleted - customers pay subscription, never touch tokens
  • Per-request x-user-id / x-org-id headers for cost attribution
  • 12 new gateway tests

Billing (Stripe + paywall)

  • subscriptions table migration (backend/migrations/001_add_subscriptions.sql) with RLS + auto-trial trigger on signup (14 days, 1M tokens)
  • Stripe SDK + webhook handler (checkout.session.completed, customer.subscription.{created,updated,deleted})
  • requireActiveSubscription middleware on chat / projectChat / tabular routes - returns 402 with reason on expired trial or quota exhaustion
  • Token usage tracked fire-and-forget after each stream completes (never blocks hot path)

Billing UI

  • Public /pricing page - Starter ($99) / Professional ($249) / Enterprise ($499+) with FAQ
  • /account/billing with token progress bar + Stripe portal button
  • TrialBanner in app chrome - shows "Trial ends in N days" + "Upgrade" link
  • 402 frontend interceptor redirects to /pricing?reason=...

Dev experience

  • NEXT_PUBLIC_DEV_AUTH_BYPASS / DEV_AUTH_BYPASS env flags - skip Supabase auth and paywall locally
  • Hard-gated by NODE_ENV !== 'production' - production deploys ignore the flag entirely
  • Amber "Dev mode" banner shown when active

Cleanup

  • 396 LOC of dead code removed (old AppSidebar, CreditsExhaustedModal, getApiKeyStatus)
  • New Playbooks page (empty state, Phase 2 will wire creation)
  • Pricing-card borders normalized to the universal border border-border hairline

Stats

  • 12 commits · 60+ files changed
  • Backend: 25/25 tests pass · tsc clean
  • Frontend: 20/20 tests pass · Next.js build clean

Reviewer notes

  1. Critical pre-deploy step: after applying 001_add_subscriptions.sql, backfill subscription rows for existing users - they predate the auto-provision trigger and will 402 on first AI call. One-liner in docs/mike-2.1-deploy-checklist.md §3.3.
  2. Env vars required: AI_GATEWAY_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_STARTER/PROFESSIONAL/ENTERPRISE. All documented in backend/.env.example.
  3. Stripe products must be created manually in the Stripe dashboard before deploy - price IDs flow through env vars.
  4. AI Gateway streaming was NOT smoke-tested against the live endpoint (no key available in this environment). Unit tests cover the call shape end-to-end via mocks. Manual curl smoke recommended before promoting to production.
  5. Open follow-ups (parked in run log): ApiKeyMissingModal / modelAvailability still imported by chat/tabular components but backend never triggers them; token counter resets only on checkout.session.completed (needs invoice.paid for monthly cycle); no seat enforcement yet; mobile chrome hides both nav columns; several feature components still use border-2 instead of hairline tokens.

Revert any individual batch

```bash git log --oneline main..HEAD # see all 12 commits git revert # undo one batch ```

Test plan

  • Apply migration 001_add_subscriptions.sql to a Supabase test project
  • Set Stripe test-mode env vars + create test products
  • cd backend && npm test → expect 25/25 pass
  • cd frontend && npm test → expect 20/20 pass
  • Frontend dev with NEXT_PUBLIC_DEV_AUTH_BYPASS=1 - verify dev banner + all routes render
  • Hit POST /chat with no subscription → 402 with {reason: 'no_subscription'}
  • Sign up new user → check subscriptions row created with tier='trial'
  • Stripe webhook test event → verify subscription row updates
  • Live AI Gateway smoke (manual curl with real AI_GATEWAY_API_KEY)

Our analysis

Fork Mike 2.0 into a paid SaaS with gateway-routed LLMs and Stripe paywall — read the full analysis →

Phase 1 UI follow-ups: /playbooks stub, pricing border normalization, not-found token migration — read the full analysis →

Dead-code sweep after Mike 2.1 + gateway migrations — read the full analysis →

NODE_ENV-gated auth + paywall bypass for local previews — read the full analysis →

Stripe subscriptions, paywall middleware, and billing UI — read the full analysis →

Auth pages migrated to Mike 2.1 with B2B-required organisation field — read the full analysis →

Chrome shell split: 64px IconRail + 224px SecondaryNav — read the full analysis →

LLM traffic routed through Vercel AI Gateway; customer-supplied keys removed — read the full analysis →

Mike 2.1 design token system + primitive redesigns — read the full analysis →

Think the analysis missed something the PR description covers?

Commits in this PR (12)

SHA Subject Author Date
3da5dd95 docs(mike-2.1): add design tokens and frontend inventory specs z 2026-05-13 ↗ GitHub
commit body
Batch 0a + 0b - pre-implementation reference docs.

- docs/mike-2.1-design-tokens.md: bespoke unprefixed token family
  (B:* nitro and F:* shadcn .pen kits are reference only, the
  unprefixed tokens are production).
- docs/mike-2.0-frontend-inventory.md: catalog of existing primitives
  + auth pages + chrome shell that Phase 1 migrates.

Agent IDs: a431cf5ab43329570 (tokens), ad8271abf2f042093 (inventory).
7b4dccea feat(design): apply Mike 2.1 tokens and redesign 4 shadcn primitives z 2026-05-13 ↗ GitHub
commit body
Batch 1 - replace globals.css @theme with bespoke Mike 2.1 token
family, rebuild Button/Input/Badge/Dropdown against the new tokens,
add vitest + 3 smoke tests.

- frontend/src/app/globals.css: bespoke unprefixed tokens (production
  family). --color-blue/--color-azure preserved as faded blue for
  legacy .usc-section / .cfr-section CSS in legal-doc viewer.
  --font-eb-garamond next/font wiring kept for legacy .font-eb-garamond
  utility.
- frontend/src/components/ui/{button,input,badge,dropdown-menu}.tsx:
  rebuilt against Mike 2.1 tokens.
- frontend/vitest.config.ts + vitest.setup.ts: test runner config.
- frontend/src/components/ui/__tests__/primitives.smoke.test.tsx:
  3 smoke tests. All pass.

Agent ID: a4c766b6e2d6a274e (general-purpose).
3c8ffa8f feat(auth): migrate login and signup to Mike 2.1 chrome z 2026-05-13 ↗ GitHub
commit body
Batch 2 - port login + signup pages from .pen frames
(S9ZQTA/tbg1h/gtQ5F login, V3Q1xu signup) to Mike 2.1 design.

- Login + signup forms rebuilt against new tokens + primitives.
- site-logo updated to Mike 2.1 mark.
- Organisation field made REQUIRED (B2B product).
- Confirm-password field preserved (functionality preservation).
- WorkOS-SSO and magic-link buttons NOT lifted (no wired primitive).
- 2 smoke tests in app/__tests__/auth-pages.smoke.test.tsx.

Agent ID: a98a89fd5a4635549 (general-purpose).
571af72d feat(chrome): split AppSidebar into 64px IconRail and 224px SecondaryNav z 2026-05-13 ↗ GitHub
commit body
Batch 3 - replace the legacy collapsible AppSidebar with the Mike 2.1
two-column nav shell (IconRail 64w + SecondaryNav 224w).

- frontend/src/components/chrome/icon-rail.tsx: top-level icon column.
- frontend/src/components/chrome/secondary-nav.tsx: contextual nav,
  includes a new Playbooks entry.
- frontend/src/app/(pages)/layout.tsx: switch to new chrome.
- frontend/src/components/chrome/__tests__/chrome.smoke.test.tsx.

.pen frame references: wF9tL, x72bX, FdYC1, yEGYQ, QxArF, CzlFI.

Notes:
- AppSidebar.tsx left on disk (removed in batch 7 cleanup commit).
- Mobile (<md) currently hides both columns; no drawer yet.
- TrialBanner mount in this layout lands in batch 6 (billing UI).

Agent ID: a36447602e8db8572 (general-purpose).
07a46ceb feat(llm): replace direct provider SDKs with Vercel AI Gateway z 2026-05-13 ↗ GitHub
commit body
Batch 4 - route all LLM traffic through the Vercel AI Gateway via
AI SDK v6. Tokens are now paid by the platform, not the customer.

Backend:
- New gateway adapter at backend/src/lib/llm/gateway.ts (uses
  ai@^6.0.182, "provider/model" strings auto-route via the bundled
  gateway provider).
- 12 unit tests in backend/src/lib/llm/__tests__/gateway.test.ts.
- Deleted backend/src/lib/llm/{claude,gemini,openai,tools}.ts and
  backend/src/lib/userApiKeys.ts - the per-provider SDKs and the
  customer-key vault are gone.
- backend/src/lib/llm/index.ts now re-exports from gateway and routes
  streamChatWithTools / completeText through it.
- backend/src/lib/llm/types.ts: added StreamChatAttribution
  ({ userId?, orgId? }) for gateway cost-slicing; UserApiKeys kept
  as @deprecated alias so existing call-sites still typecheck.
- backend/src/lib/userSettings.ts: drop per-user api_keys lookup.
- backend/src/lib/chatTools.ts: runLLMStream now forwards attribution
  instead of apiKeys and surfaces aggregate usage.totalTokens.
- backend/src/routes/{chat,projectChat,tabular}.ts: attribution
  ({ userId }) replaces apiKeys; legacy queryGemini/title helpers
  refactored to take userId.
- backend/src/routes/user.ts: deleted GET/PUT /user/api-keys.
- backend/.env.example: replaced GEMINI_/ANTHROPIC_/OPENAI_API_KEY
  and USER_API_KEYS_ENCRYPTION_SECRET with AI_GATEWAY_API_KEY.

Frontend:
- Deleted frontend/src/app/(pages)/account/models/page.tsx - the
  user-facing API key UI no longer applies.
- frontend/src/app/(pages)/account/layout.tsx: removed the
  "Models & API Keys" tab (Billing tab arrives in batch 6).

Intermediate state notes:
- frontend mikeApi.ts still exports getApiKeyStatus + saveApiKey
  against now-404 routes; UserProfileContext still imports them.
  Both are cleaned up in batches 6/8.
- public.user_api_keys table is not dropped here - orphaned but
  harmless; can be removed in a follow-up migration.

Agent ID: a5511de9cafb2355a (vercel:ai-architect).
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).
9974612f chore: remove dead code (AppSidebar, CreditsExhaustedModal, getApiKeyStatus) z 2026-05-13 ↗ GitHub
commit body
Batch 7 - refactor-cleaner sweep. Three call-site-less files
removed after the Mike 2.1 chrome migration:

- frontend/src/app/components/shared/AppSidebar.tsx (311 LOC):
  superseded by IconRail + SecondaryNav in batch 3.
- frontend/src/app/components/modals/credits-exhausted-modal.tsx
  (81 LOC): credit-system modal, no remaining importers after the
  subscription/paywall flow in batches 5-6.
- frontend/src/app/lib/mikeApi.ts getApiKeyStatus() (4 LOC):
  hits a route that no longer exists since batch 4.

Left alone (still has active importers): ApiKeyMissingModal,
modelAvailability helpers.

Agent ID: aca933ce4c3383624 (refactor-cleaner).
2034b855 docs(mike-2.1): add deploy checklist and run log z 2026-05-13 ↗ GitHub
commit body
Batch 8 - final smoke + paper trail. The deploy checklist verifies
backend + frontend builds, paywall coverage, presence of all
generated files, and lists 9 known follow-ups parked for the Phase 1
hotfix backlog. The run log is the per-batch audit trail referenced
above with the SHAs of commits 1-8.

Agent ID: a6969272e17743cb6 (general-purpose).
85348e4c feat(dev): add NODE_ENV-gated auth + paywall bypass for local previews z 2026-05-13 ↗ GitHub
commit body
Adds NEXT_PUBLIC_DEV_AUTH_BYPASS (frontend) and DEV_AUTH_BYPASS (backend)
flags. Both require non-production NODE_ENV. When on:
- Frontend skips Supabase auth, injects stub user + Professional-tier
  subscription, and shows an amber "dev mode" banner.
- Backend skips JWT verification and paywall middleware.

Production deployments ignore these flags by NODE_ENV check.
3d1b1b69 docs: record batch 10 commit SHA in run log z 2026-05-13 ↗ GitHub
82d36cca fix(ui): add /playbooks page, normalize pricing borders, migrate not-found to Mike 2.1 tokens z 2026-05-14 ↗ GitHub
commit body
- Add stub /playbooks route with empty state so the IconRail nav entry
  resolves (resolves deploy-checklist follow-up #6).
- Normalize all three pricing cards to the universal hairline
  `border border-border`; drop `border-2 border-border-active` on the
  Professional card. "Most popular" is now signaled only by the badge.
- Migrate not-found.tsx off legacy tokens (font-eb-garamond,
  text-gray-500, raw bg-gray-900 anchor) to bg-bg-canvas / foreground /
  muted-foreground + Button asChild + next/link.

Capture this PR into my fork

Download a Markdown prompt that tells Claude how to port every commit in this PR into your working tree. Run it via claude -p < capture-pull-1.md from inside the repo you want the changes in.

⬇ Download capture-pull-1.md