<!--
ROBOTS ON PAYROLL / THE VAULT
What this is: a fill-the-slots kickoff prompt that scaffolds the complete SaaS starter stack in one agent run, with a build order.
How to use it: download this file and give it to your AI (Claude, ChatGPT,
Cursor, Lovable chat) as a reference. Say: "Use this as my playbook for
scaffolding my SaaS."
Latest version + full guide: https://robotsonpayroll.com/resources/saas-starter-stack
-->

# The Kickoff Prompt

Fill the slots in the block below, then paste the whole thing into Claude Code (or Cursor agent mode) in an empty directory. It scaffolds the complete stack: Next.js + Supabase (with RLS) + Stripe + Resend + PostHog + Sentry, wired together, with a build order the agent can actually follow.

Fill these before pasting:

- `[PRODUCT_NAME]` : display name, e.g. `InvoiceOwl`
- `[PRODUCT_SLUG]` : kebab-case, e.g. `invoice-owl`
- `[ONE_LINE_PITCH]` : what it does for whom, e.g. `invoicing for freelance designers who hate spreadsheets`
- `[CORE_ENTITY]` : the main thing a user creates, singular, e.g. `invoice`
- `[CORE_ENTITY_FIELDS]` : 3-6 fields for it, e.g. `client_name text, amount_cents integer, due_date date, status text default 'draft'`
- `[FREE_PLAN_LIMIT]` : what free users get, e.g. `3 invoices per month`
- `[PRO_PRICE]` : monthly price, e.g. `$12/month`
- `[SENDER_DOMAIN]` : email domain you will verify in Resend, e.g. `mail.invoiceowl.com`

---

```
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.
```

---

## After the agent finishes

1. Do the "NEXT STEPS (human)" items. The agent cannot verify your DNS or create your Stripe account.
2. Work through `env-checklist.md` (in this same vault item) before first deploy.
3. Replace the placeholder terms/privacy pages before charging anyone real money.
