Software stack
Speedrun a full SaaS buildout with a proven stack.
1---2name: saas-stack-playbook3description: Speedrun a full SaaS buildout with a proven stack — Next.js + React for the app, Vercel for hosting, Supabase for data, Clerk for auth, Railway or Fly.io for long-running background work. Use when the user wants to build, scaffold, or deploy a web app or SaaS, asks which services to use for hosting, database, auth, or background jobs, needs the wiring between these services, or wants a project taken from zero to deployed. Four of the five services have CLIs, so an agent can execute most of this directly.4---56# The SaaS stack playbook78Every new build re-asks the same questions: which framework, where to host, where's the data, how does auth work, where do the long jobs run. This skill answers them once with a stack proven across repeated buildouts, then gets out of the way. The value is not that these are the only good tools; it's that this exact combination wires together cleanly, every service does one job, and an agent can drive four of the five from the terminal.910The stack:1112- **Next.js + React** — the app: frontend, API routes, everything user-facing13- **Vercel** — hosting and deployment for the Next.js app (CLI: `vercel`)14- **Supabase** — Postgres database, storage, realtime (CLI: `supabase`)15- **Clerk** — authentication and user management (no CLI; dashboard + env keys)16- **Railway or Fly.io** — anything long-running that serverless can't hold: workers, queues, cron, scrapers, agents (CLIs: `railway`, `fly`)1718For the UI itself, use the frontend-design-playbook skill if installed: shadcn/ui as the foundation with COSS UI, Beautiful UI, and transitions.dev layered on. This skill covers everything below the pixels.1920## The division of labor (get this right and the rest is typing)2122**Vercel runs the app, not the work.** Serverless functions are for request/response: pages, API routes, webhooks, quick mutations. They have execution time limits measured in seconds to a few minutes depending on plan and configuration, they scale to zero, and they are the wrong home for anything that runs long or must keep running. The moment a task is "scrape 500 pages", "process this video", "run this agent loop", or "poll every minute forever", it does not belong on Vercel.2324**Railway or Fly.io run the work.** A plain Node (or Python) worker in its own repo folder or repo, deployed as an always-on or scheduled process. It talks to the same Supabase database, so the app and the worker share state without any extra glue. Choosing between them: Railway is the fastest from zero (point it at the repo, it builds and runs, dashboards are simple) and is the default; Fly.io wins when you want more control, multiple regions, or to run something closer to bare metal with a Dockerfile. Pick one per project; running both buys nothing.2526**Supabase is the single source of truth.** One Postgres database holds everything: app data, job queues, user profile rows. The app reads/writes it from Next.js; workers read/write the same tables. Storage buckets hold files. Realtime subscriptions push live updates to the dashboard when workers finish things. Do not add a second database, a separate queue service, or Redis until Postgres demonstrably can't do the job; a `jobs` table with a status column covers most background-work coordination at this scale.2728**Clerk owns identity, Supabase owns data.** Clerk handles sign-up, sign-in, sessions, orgs, and the user profile UI (its prebuilt components drop straight into Next.js). The database keys rows by the Clerk user ID. Wire them at the token level: Supabase supports third-party auth with Clerk, so the Clerk session token is passed to the Supabase client and Row Level Security policies check the Clerk user ID from the JWT. Result: RLS actually protects every row, and there's no duplicate auth system. Sync a minimal `users` table via a Clerk webhook (user.created, user.updated) hitting a Next.js API route.2930## The standard buildout order3132Run these phases in order; each one is verifiable before moving on.3334**1. Scaffold.**35```36npx create-next-app@latest app-name # App Router, TypeScript, Tailwind: yes37cd app-name && git init && gh repo create # or push to existing remote38```3940**2. Auth first (Clerk).** No CLI, so this is the one dashboard trip: create the Clerk app, copy the publishable and secret keys into `.env.local`. Then in code: install `@clerk/nextjs`, wrap the root layout in `ClerkProvider`, add `middleware.ts` with `clerkMiddleware()` protecting everything except public routes, and drop `<SignInButton>` / `<UserButton>` in the header. Auth exists before any feature does, because retrofitting auth is always worse.4142**3. Database (Supabase).**43```44supabase init # local config in the repo45supabase link --project-ref <ref> # link to the hosted project46supabase migration new initial_schema # write schema as SQL migrations47supabase db push # apply to hosted db48supabase gen types typescript --linked > src/lib/database.types.ts49```50Rules: schema lives in migrations in the repo, never clicked together in the dashboard, so every environment is reproducible. Enable RLS on every table from the first migration; with the Clerk integration active, policies check the Clerk user ID claim. Generate types after every migration so the app never guesses at column names.5152**4. Wire app to data.** Supabase JS client configured to pass the Clerk session token; server components and route handlers use it for queries; RLS does the authorization. Keep one `src/lib/supabase.ts` factory and use it everywhere, because scattered client creation is where auth bugs live.5354**5. Deploy the app (Vercel).**55```56vercel link # connect repo/project once57vercel env add # or set in dashboard: Clerk keys, Supabase URL + anon key58vercel # preview deploy59vercel --prod # production60```61Connect the Git repo in Vercel so every push gets a preview URL and main auto-deploys. `vercel env pull .env.local` keeps local env in sync with the project. Ship the walking skeleton (auth + one page reading the database) to production on day one; everything after that is iteration on a live app.6263**6. Background work (Railway or Fly.io), only when a real long-running need exists.**64Worker pattern: a `worker/` directory with its own entry point, reading a `jobs` table in Supabase (status: queued → running → done/failed, with attempts and a locked_at column), processing whatever is queued, writing results back. The app enqueues by inserting a row; the dashboard shows progress via a realtime subscription on that table.65```66# Railway67railway init && railway up # deploy the worker directory68railway logs # watch it run69railway variables set KEY=value # env for the worker (service-role Supabase key lives here, never in the app)7071# Fly.io72fly launch # generates fly.toml, builds, deploys73fly deploy74fly logs75fly secrets set KEY=value76```77Cron-shaped work: Railway's scheduled jobs or a `fly machine` on a schedule; either beats stuffing cron into serverless. The worker uses the Supabase service-role key (bypasses RLS) because it acts on behalf of the system; that key exists ONLY in Railway/Fly env, never in Next.js, never in the browser.7879**7. Webhooks close the loops.** Clerk → Next.js route for user sync. Stripe (when payments arrive) → Next.js route for subscription state, written to Supabase. External services → app, always through a verified webhook route, always idempotent, because every webhook eventually fires twice.8081## Environment variable hygiene8283Three environments (local, preview, production), one rule: a variable exists in the place that uses it and nowhere else.8485- App (Vercel + `.env.local`): Clerk publishable + secret keys, Supabase URL + anon key, any public config86- Worker (Railway/Fly): Supabase URL + service-role key, third-party API keys the worker uses87- Never in the browser: anything named "secret" or "service"; `NEXT_PUBLIC_` prefix is a deliberate act, not a default88- `vercel env pull` after any change so local matches; a `.env.example` in the repo lists every required key with placeholders so the next session (or the agent) knows what must exist8990## Decision rules, so the agent doesn't re-litigate the stack9192- Long-running, scheduled, or always-on → Railway/Fly. Request-response → Vercel. No exceptions "just this once"; the exception becomes the outage.93- Data → Supabase Postgres until proven otherwise. Queue → a jobs table until proven otherwise. Files → Supabase storage.94- Auth → Clerk, full stop; never hand-roll sessions next to it, never mix in Supabase Auth in the same app, one identity system per app.95- New service to the stack only when a phase of this playbook genuinely can't cover the need, and it gets added to the project's README with one line on why.96- Default region: put Supabase, the workers, and Vercel's functions in the same region; cross-region chat between app, database, and worker is the silent latency tax.9798## When something breaks99100- Auth loops or 401s: middleware matcher first (is the route accidentally protected/unprotected), then token passing to Supabase (is the client actually sending the Clerk token), then RLS policy (does it check the right claim).101- Works locally, fails deployed: it's env vars, roughly always. `vercel env ls`, compare against `.env.example`.102- Worker silent: `railway logs` / `fly logs` before any code changes; then check the jobs table for rows stuck in `running` (a crashed worker mid-job) and add the locked_at timeout sweep if it's missing.103- Slow pages: check for client-side fetching what a server component should fetch, then for missing database indexes on the columns the page filters by (`supabase migration new add_indexes`).104- Type errors after schema change: regenerate (`supabase gen types`), because the types file is a build artifact, not a source file.105106## Running this skill107108When the user says "build me X": confirm only what's genuinely undecided (name, and whether background work exists in v1), then execute the buildout order top to bottom with the CLIs, announcing each phase's verification (auth works, table reads, deploy URL live) as it lands. When the user has an existing project: map it against the division of labor, flag anything living in the wrong place (long jobs on Vercel is the usual finding), and migrate in the buildout order. When the user asks a "which service" question: answer from the decision rules and move on, because the point of a standard stack is not spending Tuesday re-choosing it.