Three skins, one brain, and a monorepo that fought back.
The tech stack
Next.js 16 on Vercel, Clerk for auth and billing, Supabase Postgres for data, Gemini for the reframes, Resend for mail, Tailwind v4 for the surface. Nothing exotic — the difficulty was never any single piece, it was the seams between them. Three of those seams did most of the fighting.
The Clerk ↔ Supabase bridge
Auth and billing run on Clerk; data on Supabase Postgres with row-level security keyed to the Clerk JWT — a requesting_user_id() function reads sub out of the token so every policy restricts rows to the owning user. Getting Clerk-issued JWTs to satisfy Postgres RLS cleanly was the fiddliest seam in the app.
Tiers, usage, and gated migrations
Three tiers, enforced server-side in the generate-response route: anonymous (3 lenses/day, in localStorage), free (daily quote + lens limits in a usage_log table), and pro (unlimited, $8.99/mo via Clerk Billing). Migrations follow a gated process — authored as the next contiguous NNN_name.sql, validated in CI, applied deliberately — after an earlier stretch of hand-applied SQL caused real schema drift.
Host-split routing
One codebase serves two faces: the marketing landing at the apex and the product at app.minds-shift.com, split by a proxy that redirects on exact production hostnames only (so localhost and previews stay single-host).
Three skins, one brain — and Storybook
Cyberpunk, Kawaii, and Notepad aren't re-skins — they're three complete token systems (Tailwind v4 + CSS custom properties, one file each) swapped by a single data-theme attribute on <html>. One brain, three worlds, no forked components. The catch with three worlds is that a fix in one can silently break the other two, so every shared component gets built and reviewed in Storybook across all three themes at once — the catalog is what keeps the insanity of three skins honest instead of turning into three diverging codebases.
Shipping to the App Store
MindShift is live on the web today at minds-shift.com. The next milestone — not yet done — is a native App Store presence: wrapping the product for iOS, reworking the tier and billing flow to sit inside App Store rules, and earning the trust bar a mental-wellbeing app has to clear. It's the front I'm pointing at next, not something I'm claiming yet.
Field notes
The migration tree that ate an afternoon
Reordering RLS policies broke inserts — requesting_user_id() returned null because the JWT claim wasn't set on the server path. RLS failures are silent: they read as "no rows," not "permission denied." The fix was setting the claim on the server client before the insert.