robots on payroll.

THE VAULT / RUN IT LIKE A BUSINESS / GUIDE + PROMPT + MARKDOWN

The SaaS Starter Stack

This is the toolchain I'd hand anyone shipping a SaaS alone with AI agents in 2026: seven tools, $20-29/mo at 100 users, roughly $120-250/mo at 10K. The centerpiece is a kickoff prompt you paste into Claude Code that scaffolds the whole thing wired together, auth and RLS and Stripe webhooks included, so you own every line instead of renting a $300 boilerplate.

LAST VERIFIED 2026-07-09

NOTE

Use this page as a prompt reference. Every file below is built to be handed straight to an AI. Download it, drop it into Claude, ChatGPT, Cursor, or Lovable, and say "use this as my playbook." The prompts on this page are copy-paste ready.

The $300 boilerplate is dead. The prompt is the boilerplate now.

For a decade the solo-SaaS move was: buy a $199-$349 boilerplate, spend a week deleting the 60% you don't need, then live with someone else's architecture forever. That trade made sense when scaffolding an app cost you a month. It doesn't anymore. An agent with a good enough prompt scaffolds the same app in an afternoon, and every line in the repo is yours.

That flips the economics. A boilerplate is a depreciating asset: it drifts out of date, its auth library gets abandoned (RIP NextAuth, now in maintenance mode after Better Auth absorbed it in early 2026), and you can't ask it questions. A kickoff prompt is the opposite: you update one text file when the ecosystem moves, and the agent regenerates fresh, current code on demand.

You own every line is not a slogan, it's the operational requirement. When your app breaks at 11pm, you debug it with an agent, and the agent is dramatically better at code it wrote in your conventions than at 40K lines of someone else's cleverness. Small surface, boring choices, your name on every file.

This item is the whole system: the stack I'd pick today as one person shipping with AI agents, real costs at 0 / 100 / 10K users, the one gotcha per tool that actually bites, and a kickoff prompt you paste into Claude Code that scaffolds the entire thing wired together (auth, Postgres with RLS, Stripe, transactional email, analytics, error tracking).

NOTE

The selection rule behind every pick: when an agent writes 90% of your code, choose the tool the agent is best at, not the tool with the best benchmark. Next.js over SvelteKit is the clearest example: SvelteKit polls at 93% dev satisfaction, but AI coding tools are measurably stronger at React/Next.js. The training-data moat compounds in your favor every single session.

The stack, with real prices at 0 / 100 / 10K users

Prices verified July 2026. "~100 users" means early MVP traffic, "~10K users" means a real growing app. Two totals to anchor on: $20-29/mo at 100 users and roughly $120-250/mo at 10K users, plus Stripe's cut of revenue.

LayerTool0 users~100 users~10K users
FrameworkNext.js App Router + TS + Tailwind + shadcn/ui$0$0$0
HostingVercel$0 (Hobby, pre-launch only)$20 Pro (required for commercial use)$20-60
DB + auth + storageSupabase$0$0 (free tier covers it)$25 Pro
PaymentsStripe$0~2.9% + $0.30 per chargesame rates; Billing adds flat 0.7%
Transactional emailResend$0$0 (3K/mo free)$20 Pro (50K/mo)
Product analyticsPostHog$0$0 (1M events/mo free)$0-50
Error trackingSentry$0$0 (5K errors/mo free)$26 Team annual
Total fixed$0$20-29/mo~$120-250/mo

Later additions, only when triggered: Upstash Redis ($0-10, first rate-limit or queue need), Cloudflare R2 (~$1-10, heavy files, $0 egress always), Loops ($0 to 1K contacts, then $49+, marketing email once you have a list), Plausible ($9-19, optional cookieless web analytics).

WATCH OUT

Three 2026 pricing changes people still get wrong: Clerk killed its famous 10K-MAU free tier on Feb 5, 2026 (replaced with 50K monthly retained users, actually more generous). Neon cut storage from $1.75 to $0.35/GB-mo after the Databricks acquisition in May 2026 and removed the monthly minimum. Stripe Billing consolidated to a flat 0.7% of billing volume. Any stack advice quoting the old numbers is stale.

PAYOFF

The bottom line at MVP: the only money anyone forces you to spend at 100 users is Vercel Pro at $20/mo, because Hobby is strictly non-commercial and hard-stops on overage. Everything else rides free tiers that are genuinely generous in 2026.

Every tool, what it does, and the one gotcha that actually bites

One tool per job, one gotcha each. These are the failure modes I see over and over, not theoretical ones.

Next.js App Router: the agent-fluency pick

Framework, routing, server components, API routes. Not the most loved framework of 2026 (RSC complexity and Vercel lock-in grumbling are real), but it's the one your agent one-shots. Remix merged into React Router v7 and is no longer a mainstream new-project pick; SvelteKit is lovely and a worse agent target. Decision made.

WATCH OUT

Gotcha: agents love sprinkling "use client" everywhere because it makes errors go away. Every one of those turns a server component into shipped JavaScript. Tell the agent (the kickoff prompt does): server components by default, client components only where interactivity requires it.

Vercel: deploys you never think about

Push to main, it's live. Preview URL per branch. Zero config for Next.js. This is $20/mo of not being your own DevOps team.

WATCH OUT

Gotcha: the Hobby plan is non-commercial, and Vercel enforces it. The moment you take money you need Pro, and Hobby hard-stops on usage overages with a 30-day wait to unfreeze. Upgrade before your first paying customer, not after your site goes dark mid-launch.

Supabase: Postgres, auth, and storage in one dashboard

The all-in-one pick: managed Postgres, auth (email + OAuth), and file storage from one vendor with one client library. The killer feature is that auth identities live in the same Postgres as your data, which makes Row Level Security practical: the database itself refuses to return rows the user doesn't own, even when your agent writes a buggy query. Free tier: 500 MB DB, 50K auth MAU, 1 GB storage.

WATCH OUT

Gotcha: free-tier projects pause after 1 week of inactivity. Fine while building, a self-inflicted outage at launch. Go Pro ($25/mo, includes backups) the day you launch. Second, sharper gotcha: the service-role key bypasses RLS entirely. It belongs in exactly one server-only file, and prefixing it with NEXT_PUBLIC_ is the single most catastrophic typo available in this stack.

Stripe: payments, unchanged and unmatched

2.9% + $0.30 per US online card charge, same as it's been for years. Checkout for purchase, Customer Portal for self-serve management (cancellations you never have to email about), webhooks for truth. Stripe Billing for subscriptions is now a flat 0.7% of volume.

WATCH OUT

Gotcha: the webhook is the only truth. If any code path other than your verified webhook handler sets a user's plan, you have a free-Pro exploit: anyone can hit your success URL without paying. Verify signatures against the raw request body, handle events idempotently, and never trust the client's word that money moved.

Resend: transactional email in 10 minutes

Welcome emails, receipts, password resets. react-email templates your agent writes like any other React component. 3,000 emails/mo free.

WATCH OUT

Gotcha: the free tier's 100/day cap is the one that bites, right when a launch spike sends 300 welcome emails in an afternoon. Also: send from a subdomain (mail.yourproduct.com) so your transactional reputation is isolated, and never send marketing through it. Deliverability is a savings account; newsletters from your receipt domain are a withdrawal.

PostHog: analytics you won't pay for

Product analytics, session replays, and feature flags in one, 1M events/mo free. 98% of PostHog's customers pay $0, which is the correct amount to pay for analytics before you have users to analyze.

WATCH OUT

Gotcha: session replays, not events, are the first thing that costs money. Set billing limits in the PostHog dashboard on day one so a traffic spike is a happy surprise instead of an invoice.

Sentry: hear about bugs before users email you

Error tracking, client and server, 5K errors/mo free forever on the Developer plan. At one-person scale this is the difference between fixing a bug in an hour and losing the customer who hit it.

WATCH OUT

Gotcha: two setup mistakes neuter it. Skip source-map upload and every production error is minified soup. Skip the tunnel route and ad-blockers silently eat a chunk of your client-side error reports. The kickoff prompt wires both.

The bench: Upstash, R2, Loops

Not in v1 on purpose. Upstash Redis joins at your first abuse incident or first real background job ($0-10). Cloudflare R2 joins when user files outgrow Supabase storage ($0.015/GB-mo, $0 egress always). Loops joins at 500+ signups when you have a list worth emailing. Adding tools before their trigger is how solo projects turn into unpaid DevOps jobs.

The two forks worth arguing about (auth and database)

Two picks in this stack have credible alternatives, and the right answer depends on you, not the tools.

DecisionPick APick BThe rule
AuthSupabase Auth (bundled, pairs natively with RLS via auth.uid())Clerk ($0 to 50K MRU, $25 Pro; prebuilt UI, orgs)Bundled auth if you want one vendor and RLS wired in a day. Clerk if you want the prettiest prebuilt UI and B2B orgs, but note the orgs add-on is $100/mo, which is the real cost driver, and your user data lives in their system.
Auth (self-host)Better Auth ($0, MIT, your DB)The 2026 self-hosted default (28.6K+ GitHub stars, absorbed Auth.js/NextAuth, which is now maintenance mode). $0 at any scale and full data ownership, at the cost of wiring it to RLS yourself.
DatabaseSupabase (DB + auth + storage bundle)Neon + Drizzle (DB only, post-May-2026 pricing is very cheap: $0.35/GB-mo storage, no minimum)Supabase if you want one dashboard and the RLS + auth pairing. Neon if you only need Postgres, love branch-per-PR databases, and are pairing with Better Auth anyway.

The kickoff prompt below commits to Supabase-for-everything because it's the fewest moving parts for one person. If you fork to Neon + Better Auth, the prompt's global rules (RLS everywhere, webhook-only subscription writes, validated env) transfer unchanged; swap Phase 2 and 3 details.

The kickoff prompt: paste once, get the whole stack wired

This is the flagship artifact. Fill 8 slots, paste into Claude Code (or Cursor agent mode) in an empty directory, and it scaffolds the complete app: Next.js + Supabase with RLS on every table, Stripe Checkout + Portal + idempotent webhooks, Resend transactional email, PostHog events, Sentry, zod-validated env, plus a phase-by-phase build order with verification gates and a final self-audit the agent must print PASS/FAIL for.

  1. 1

    Fill the 8 slots

    [PRODUCT_NAME], [PRODUCT_SLUG], [ONE_LINE_PITCH], [CORE_ENTITY] (the main thing users create, e.g. invoice), [CORE_ENTITY_FIELDS] (3-6 SQL fields for it), [FREE_PLAN_LIMIT], [PRO_PRICE], [SENDER_DOMAIN]. The download file lists an example value for each.

  2. 2

    Paste into an empty directory

    Claude Code or Cursor agent mode. Let it run the phases in order; the prompt forbids skipping ahead and requires a verification step after each phase.

  3. 3

    Watch Phase 8

    The agent must print PASS/FAIL for 9 security and correctness checks, each with the file that proves it. If anything fails, tell it to fix and re-run the self-check. Do not skim this part; it's the whole point.

  4. 4

    Do the human steps

    The agent finishes by printing a NEXT STEPS (human) list: create the Supabase project, Stripe product + price, verify your Resend domain, set Vercel env vars. Agents cannot verify your DNS. Then work through the env checklist below.

PROMPT: The kickoff prompt (fill the [SLOTS], paste the whole thing)
You are scaffolding a production SaaS from zero in this empty directory. Work through the phases in order. Do not skip ahead. After each phase, run the verification step before moving on, and fix anything that fails.

# Product

- Name: [PRODUCT_NAME]
- Slug: [PRODUCT_SLUG]
- Pitch: [ONE_LINE_PITCH]
- Core entity users create: [CORE_ENTITY]
- Plans: Free ([FREE_PLAN_LIMIT]) and Pro ([PRO_PRICE], unlimited)

# Stack (fixed, do not substitute anything)

- Next.js (latest stable) App Router, TypeScript strict mode, src/ directory, ESLint
- Tailwind CSS + shadcn/ui for all UI. No other component or CSS libraries.
- Supabase: Postgres, Auth, and Storage. @supabase/ssr for cookie-based auth in the App Router.
- Stripe: Checkout for subscription purchase, Customer Portal for management, webhooks for state.
- Resend + react-email for transactional email, sending from [SENDER_DOMAIN].
- PostHog (posthog-js on client, posthog-node on server) for product analytics.
- Sentry (@sentry/nextjs) for error tracking, client and server.
- zod for env validation and all input validation.

# Global rules (these override anything you would normally do)

1. Every database table gets RLS enabled and explicit policies IN THE SAME MIGRATION that creates it. A table without policies is a bug, even if it "works".
2. The Supabase service-role key is used ONLY inside src/lib/supabase/admin.ts, and that module must start with `import "server-only"`. Nothing under any client component may import it.
3. Subscription state is written ONLY by the Stripe webhook handler. No other code path ever sets a user's plan. Client-side "success" pages read state, never write it.
4. Every env var is read through one validated config module (zod schema, parsed at boot). No `process.env.X` scattered through the codebase. Missing required vars must crash the app at startup with the variable name in the error.
5. Amounts are integer cents everywhere. Never floats for money.
6. Every server action and API route validates its input with zod before touching the database.
7. Generate a .env.example listing every variable with placeholder values and a one-line comment each. Never write real secrets to any committed file.
8. Prefer server components; use "use client" only where interactivity requires it.
9. Do not invent extra features, admin panels, or settings beyond what is specified. Small surface, finished, correct.

# Phase 1: Scaffold

Create the Next.js app with TypeScript, Tailwind, ESLint, App Router, src/ dir. Initialize shadcn/ui with the default theme. Install all dependencies for the stack above. Create this structure:

- src/app/(marketing)/page.tsx        landing page
- src/app/(marketing)/pricing/page.tsx
- src/app/(auth)/login/page.tsx
- src/app/(auth)/signup/page.tsx
- src/app/(app)/dashboard/page.tsx    auth-required
- src/app/(app)/settings/page.tsx     profile + billing management
- src/app/api/webhooks/stripe/route.ts
- src/lib/env.ts                      zod-validated env config
- src/lib/supabase/client.ts, server.ts, admin.ts, middleware.ts
- src/lib/stripe.ts
- src/lib/email/ (Resend client + react-email templates)
- src/lib/analytics/ (PostHog client + server helpers)
- supabase/migrations/
- STACK.md placeholder, .env.example, README.md with local setup steps

Verify: `npm run build` succeeds with placeholder env values.

# Phase 2: Database with RLS

Write supabase/migrations/0001_init.sql containing:

1. `profiles` table: id uuid primary key references auth.users on delete cascade, email text, full_name text, created_at timestamptz default now(). Trigger on auth.users insert that auto-creates the profile row.
2. `subscriptions` table: user_id uuid primary key references profiles, stripe_customer_id text unique, stripe_subscription_id text, plan text not null default 'free' check (plan in ('free','pro')), status text, current_period_end timestamptz, updated_at timestamptz.
3. `[CORE_ENTITY]s` table: id uuid default gen_random_uuid() primary key, user_id uuid not null references profiles on delete cascade, [CORE_ENTITY_FIELDS], created_at timestamptz default now().

RLS for all three tables:
- profiles: users can select and update only their own row (auth.uid() = id). No insert policy (trigger handles it), no delete policy.
- subscriptions: users can select only their own row. NO insert/update/delete policies for users at all; only the service role (which bypasses RLS) writes here, from the webhook.
- [CORE_ENTITY]s: users can select/insert/update/delete only rows where user_id = auth.uid(), with the insert policy also enforcing user_id = auth.uid() via WITH CHECK.

Also create a Postgres function `get_user_plan(uid uuid)` (security definer) returning the plan, defaulting to 'free' when no subscription row exists.

Verify: migration applies cleanly with `supabase db reset` on a local instance if available; otherwise lint the SQL and re-read every policy against the rules above.

# Phase 3: Auth

Implement email+password and Google OAuth via Supabase Auth using @supabase/ssr:
- login and signup pages using shadcn form components, zod validation, real error states (wrong password, unconfirmed email, rate limited)
- OAuth callback route at src/app/auth/callback/route.ts exchanging the code for a session
- Next.js middleware that refreshes the session and redirects unauthenticated users hitting (app) routes to /login, and authenticated users hitting /login to /dashboard
- logout server action
- the dashboard page reads the session server-side and greets the user by email

Verify: build passes; walk through the file flow and confirm no client component imports the server or admin Supabase client.

# Phase 4: Payments

- src/lib/stripe.ts: Stripe client pinned to the current API version.
- Pricing page: Free vs Pro ([PRO_PRICE]) cards. Pro CTA hits a server action that creates a Stripe Checkout Session (mode: subscription, price from env STRIPE_PRICE_ID_PRO, client_reference_id = user id, customer_email = user email, success_url /dashboard?upgraded=true, cancel_url /pricing).
- Settings page: shows current plan (from subscriptions via get_user_plan) and a "Manage billing" button that creates a Stripe Customer Portal session.
- Webhook handler at /api/webhooks/stripe:
  - verifies the signature with STRIPE_WEBHOOK_SECRET against the RAW request body (read the raw body, not parsed JSON)
  - handles checkout.session.completed (create/update subscription row: customer id, subscription id, plan 'pro', status, current_period_end), customer.subscription.updated (sync status, plan, period end; downgrade to 'free' when status is not active or trialing), customer.subscription.deleted (set plan 'free', status 'canceled')
  - idempotent: processing the same event twice must not corrupt state (upsert by user_id, and log+skip unknown event types)
  - uses the admin Supabase client (service role), returns 200 fast, wraps handler logic so a thrown error returns 400 and gets captured by Sentry
- Plan gating: a server-side helper requirePlan('pro') plus enforcement of [FREE_PLAN_LIMIT] on the [CORE_ENTITY] create action, returning a typed error the UI turns into an upgrade CTA linking to /pricing.

Verify: build passes. Print for me the exact `stripe listen --forward-to localhost:3000/api/webhooks/stripe` command and the test-mode flow I should click through.

# Phase 5: Transactional email

- Resend client in src/lib/email/resend.ts using RESEND_API_KEY, from address `[PRODUCT_NAME] <hello@[SENDER_DOMAIN]>` via env.
- react-email templates: WelcomeEmail (sent after first login, checked via a welcomed_at column or metadata flag, never sent twice) and UpgradeReceiptEmail (sent from the checkout.session.completed webhook branch).
- Email sends must never break the main flow: wrap in try/catch, report failures to Sentry, continue.

Verify: build passes; confirm both templates render with the react-email preview script added to package.json.

# Phase 6: Analytics + error tracking

- PostHog: provider component in the app layout (respect NEXT_PUBLIC_POSTHOG_KEY absent = no-op), identify users on login with their user id, capture events: signed_up, logged_in, [CORE_ENTITY]_created, checkout_started, upgraded_to_pro, subscription_canceled (last two fired server-side from the webhook via posthog-node with immediate flush).
- Sentry: run the standard @sentry/nextjs setup (client, server, edge configs + instrumentation), DSN from env, no-op when the DSN is absent so local dev works without it, tunnelRoute enabled so ad-blockers do not eat error reports.

Verify: build passes with and without the PostHog/Sentry env vars set.

# Phase 7: The product itself

- Dashboard: list the user's [CORE_ENTITY]s (server component), empty state with a create CTA, create form in a shadcn dialog (server action + zod), delete with confirmation. Free-plan limit enforced server-side with the upgrade CTA from Phase 4.
- Landing page: hero with the pitch "[ONE_LINE_PITCH]", 3 feature bullets derived from the core entity, pricing teaser, footer with links to /pricing, /login, and placeholder /terms and /privacy pages (generate simple placeholder text marked as PLACEHOLDER, DO NOT SHIP AS-IS).
- Clean, minimal, shadcn defaults. Do not invent a brand.

# Phase 8: Final self-check

Run through this list and print PASS/FAIL for each line with the file that proves it:
1. Every table in every migration has RLS enabled and explicit policies.
2. Service-role key appears only in src/lib/supabase/admin.ts, which imports "server-only".
3. No code path outside the webhook writes to subscriptions.
4. Webhook verifies signatures against the raw body and is idempotent.
5. All env access goes through src/lib/env.ts; grep proves no stray process.env.
6. .env.example lists every required variable with placeholders and comments.
7. All money values are integer cents.
8. `npm run build` passes clean.
9. README documents: local setup, Supabase migration command, Stripe CLI webhook testing, and the deploy checklist.

Then print a short "NEXT STEPS (human)" list: creating the Supabase project, Stripe product + price, Resend domain verification, and the Vercel env vars to set. Do not attempt to do these yourself.

NOTE

Why the phases and PASS/FAIL gates matter: a one-paragraph "build me a SaaS" prompt produces a demo. This prompt produces a system, because every dangerous decision is pre-made as a numbered global rule (RLS in the same migration, service-role key in one server-only file, webhook-only subscription writes, integer cents) and the agent has to prove compliance at the end instead of you auditing 4,000 lines by hand.

WATCH OUT

Two things the prompt deliberately does NOT do: it generates placeholder terms/privacy pages marked DO NOT SHIP AS-IS (get real ones before charging money), and it never touches live keys or DNS. Any prompt that claims to handle those for you is lying to you.

The env var checklist (where every key comes from)

Fourteen variables across five services, plus your app URL. The full download maps each one to the exact dashboard page it comes from, flags which are browser-safe vs server secrets, and covers the test-vs-live splits. The condensed pre-deploy sweep:

  • [ ]Supabase: URL + anon key (browser-safe only because RLS exists) + service-role key (server-only, never NEXT_PUBLIC_). Auth redirect URLs include production domain and localhost.
  • [ ]Stripe: secret key, publishable key, webhook secret, Pro price id. Test keys locally, live keys in Vercel production only. The local stripe listen webhook secret and the production dashboard webhook secret are different strings.
  • [ ]Stripe live mode: production webhook endpoint created and subscribed to checkout.session.completed, customer.subscription.updated, customer.subscription.deleted. Live-mode price id set (test ids silently fail live checkouts). Customer Portal configured.
  • [ ]Resend: API key + from address on a verified subdomain (mail.yourproduct.com) with SPF, DKIM, DMARC passing. Test email sent from production to yourself.
  • [ ]PostHog: key + host set, billing limits set in the dashboard, app boots cleanly without the vars.
  • [ ]Sentry: DSN + auth token (build-time, for source maps). Deliberate test error thrown in production shows a readable stack trace.
  • [ ]Hygiene: .env.example complete with placeholders, .env.local gitignored, boot-with-a-var-missing crashes naming the var, grep for sk_live / whsec / service_role returns zero hits outside .env.local.

Launch-day checklist (the 60 minutes before you post the link)

Everything here is testable in about an hour, and every line is a real failure I've seen someone hit on launch day.

  • [ ]Vercel Pro active (Hobby is non-commercial and hard-stops on traffic spikes, with a 30-day wait).
  • [ ]Supabase Pro active (free projects pause after 1 week idle; backups on).
  • [ ]The money path, end to end in live mode: real card, smallest plan, watch the webhook fire in the Stripe dashboard, confirm the subscriptions row flips to pro, confirm the app unlocks. Then refund yourself.
  • [ ]The cancel path: Customer Portal opens, cancel works, webhook downgrades the row to free.
  • [ ]Signup path on a phone, in a private window: email confirm arrives (check spam), Google OAuth round-trips, redirect lands on /dashboard.
  • [ ]Welcome email arrives from your verified subdomain and passes SPF/DKIM (check the headers once).
  • [ ]Sentry receives a deliberate production error with readable source maps; alert email/Slack actually notifies you.
  • [ ]PostHog events flowing: signed_up and upgraded_to_pro visible in the live events feed. Billing limits set.
  • [ ]Free-plan limit enforced server-side: create past the limit as a free user and confirm the block + upgrade CTA (test in the API, not just the UI).
  • [ ]RLS spot-check: with two test accounts, confirm account B cannot read account A's rows via the anon key.
  • [ ]Real terms and privacy pages replaced the generated placeholders.
  • [ ]Status plan: you know where Vercel, Supabase, and Stripe status pages live, and your domain's DNS TTL is not 24 hours.

PAYOFF

The 80/20 of this list: if you only have 15 minutes, run the live-mode money path and the RLS spot-check. Payments and data isolation are the two failures you cannot apologize your way out of.

Deep-dives: the three questions you'll have in month 3

None of these belong in v1. All three will come up. Answers pre-cached.

Multi-tenancy later: how to not paint yourself into a corner

The kickoff prompt scaffolds single-user ownership: every row has a user_id and RLS checks auth.uid(). That's correct for v1. The month-3 question is "a customer wants to invite their teammate", and the answer is a tenancy layer, not a rewrite.

The migration path that works: add an organizations table and a memberships table (user_id, org_id, role), backfill one personal org per existing user, add org_id to your core tables alongside user_id, then flip RLS policies from user_id = auth.uid() to org_id IN (select org_id from memberships where user_id = auth.uid()). Ship it behind a feature flag, migrate your own account first.

WATCH OUT

The corner you must not paint: scattering user_id = auth.uid() logic through application code instead of keeping ownership checks in RLS policies. If ownership lives in one place (the database), multi-tenancy is a policy rewrite. If it lives in 40 query call-sites, it's an archaeology project.

Do it single-user first anyway. Orgs, invites, roles, and per-seat billing roughly triple auth complexity, and most products die with zero customers asking for teams. The trigger to build it: the second paying customer asks, or your ICP is teams from day one (in which case reconsider Clerk, whose $100/mo B2B add-on buys you orgs, invites, and role UI off the shelf).

When to add a queue (and why Vercel functions are your queue until then)

You do not have a queue problem at launch. Webhook processing, welcome emails, and analytics flushes all fit inside a request cycle. The kickoff prompt's pattern (return 200 fast, try/catch side effects, report failures to Sentry) covers you to thousands of users.

The three real triggers: (1) any job that can exceed your function timeout (bulk exports, AI generation jobs, big imports), (2) any work that must survive a failed request and retry (billing reconciliation, delivery to a flaky third-party API), (3) scheduled work beyond a single cron hitting an endpoint.

When a trigger fires, the 2026 solo-builder ladder: Vercel cron + an endpoint (free, fine for daily jobs), then Upstash QStash or Redis-backed queues (serverless, pay-per-message, no worker to babysit, free tier covers 500K commands/mo), then a real worker process on a $5 VPS only if you need long-running compute. Skip rungs and you're maintaining infrastructure nobody asked for.

NOTE

Rule of thumb: a queue earns its place when losing a job costs you money or trust. Welcome email fails? Sentry tells you, you shrug. Billing reconciliation fails silently? That's a queue with retries and a dead-letter alert.

Migrating off free tiers: the order, the triggers, the traps

Free tiers in 2026 are genuinely generous, and the correct posture is to ride them without shame. But each has a cliff, and you want to jump before you're pushed.

ServiceUpgrade triggerCostThe trap if you wait
Vercel Hobby to ProFirst dollar of revenue$20/moNon-commercial enforcement plus hard usage stops with a 30-day unfreeze wait. This one you upgrade preemptively.
Supabase Free to ProLaunch day$25/moProject pauses after 1 week idle, and free has no real backups. You want backups before you have data worth crying about.
Resend Free to Pro~70 emails/day sustained$20/moThe 100/day cap silently defers your welcome emails during the exact spike when first impressions matter.
PostHogUsually never at this scale$0-50Replays past the free allowance. Set billing limits and forget it.
Sentry Dev to TeamErrors regularly past 5K/mo or you add a teammate$26/mo annualRate-limited error ingestion during an incident, i.e. blind exactly when you need eyes.

The bigger migration question is Vercel itself. The grumbling is real: at sustained scale, Vercel compute costs multiples of a VPS. The math that matters: your time. At $200/mo hosting you could self-host for ~$40 on a VPS with Coolify or Docker, spending maybe a day a month on ops. If your product makes $5K/mo, that day is worth more than the $160 saved. Revisit at $200/mo sustained hosting spend, not before, and let an agent write the migration (it's a Dockerfile and a reverse proxy, not a mystery).

WATCH OUT

The trap on all of these: migrating in a panic. Every migration on this page is a calm afternoon if you do it at 80% of the limit, and a fire drill at 105%. Put a monthly 10-minute calendar slot on checking your usage dashboards. That's the whole discipline.

Take the files

Three artifacts, all built to live in or next to your repo. STACK.md doubles as agent context: an agent that reads it writes stack-compliant code on the first pass instead of the third.

Ship the boring stack. Spend the creativity on the product. When something here goes stale (and pricing pages guarantee something will), the fix is a one-line edit to a prompt you own, which is the whole argument of this page.

Get the next drop

Every new Vault system ships to the list first. Twice a week, free.