<!--
ROBOTS ON PAYROLL / THE VAULT
What this is: every environment variable the SaaS starter stack needs, where to get each one, and where it lives.
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
setting up my env vars safely."
Latest version + full guide: https://robotsonpayroll.com/resources/saas-starter-stack
-->

# Env Var Checklist

Every variable the stack needs, where to get it, and where it lives. Work top to bottom before your first deploy.

Ground rules:

- `NEXT_PUBLIC_*` vars ship to the browser. Anything without that prefix is a server secret. If you are ever unsure whether a key is safe to expose, it is not.
- Local secrets go in `.env.local` (gitignored). Production secrets go in the Vercel dashboard (Project, Settings, Environment Variables), scoped per environment.
- Keep `.env.example` committed with placeholders only. If a real key ever lands in git history, rotate it. Deleting the commit is not enough.
- Use separate Supabase projects and Stripe test/live modes for dev vs production. Never point local dev at the production database.

## App

| Variable | Secret? | Where to get it |
|---|---|---|
| `NEXT_PUBLIC_APP_URL` | No | `http://localhost:3000` locally, `https://yourproduct.com` in prod. Used for redirects and email links. |

## Supabase

| Variable | Secret? | Where to get it |
|---|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | No | Supabase dashboard, Project Settings, API. |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | No (RLS is what protects you) | Same page. Safe in the browser ONLY because every table has RLS. |
| `SUPABASE_SERVICE_ROLE_KEY` | YES, highest severity | Same page. Bypasses RLS entirely. Server-only, used by the Stripe webhook. If this leaks, attackers own your database. |

- [ ] Both keys set locally and in Vercel (all environments)
- [ ] Service-role key is NOT prefixed with `NEXT_PUBLIC_` (check twice, this is the classic fatal typo)
- [ ] Supabase Auth: Site URL and Redirect URLs include your production domain AND `http://localhost:3000`
- [ ] Google OAuth credentials created (Google Cloud Console) and pasted into Supabase Auth providers, with the Supabase callback URL added in Google

## Stripe

| Variable | Secret? | Where to get it |
|---|---|---|
| `STRIPE_SECRET_KEY` | YES | Stripe dashboard, Developers, API keys. `sk_test_...` locally, `sk_live_...` in prod. |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | No | Same page (`pk_test_...` / `pk_live_...`). |
| `STRIPE_WEBHOOK_SECRET` | YES | Two different values: locally from `stripe listen` output (`whsec_...`), in prod from the webhook endpoint you create in the dashboard. They are NOT the same string. |
| `STRIPE_PRICE_ID_PRO` | No, but env-specific | Create the product + recurring price in Stripe, copy the `price_...` id. Test mode and live mode have DIFFERENT price ids. |

- [ ] Test keys locally, live keys in Vercel production only
- [ ] Prod webhook endpoint created at `https://yourproduct.com/api/webhooks/stripe` subscribed to: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`
- [ ] Live-mode price id set in Vercel production (the test-mode id will silently fail live checkouts)
- [ ] Customer Portal configured in Stripe (Settings, Billing, Customer portal), or the manage-billing button will error

## Resend

| Variable | Secret? | Where to get it |
|---|---|---|
| `RESEND_API_KEY` | YES | Resend dashboard, API Keys. |
| `EMAIL_FROM` | No | e.g. `YourProduct <hello@mail.yourproduct.com>`. Must be on a verified domain. |

- [ ] Sending domain added in Resend and DNS records (SPF, DKIM, and DMARC) verified. Use a subdomain like `mail.yourproduct.com` so transactional reputation is isolated from your root domain
- [ ] Test email sent to yourself from production before launch
- [ ] Free tier limits understood: 3,000/mo AND 100/day (as of July 2026). The daily cap is the one that bites during a launch spike

## PostHog

| Variable | Secret? | Where to get it |
|---|---|---|
| `NEXT_PUBLIC_POSTHOG_KEY` | No | PostHog, Project Settings. |
| `NEXT_PUBLIC_POSTHOG_HOST` | No | `https://us.i.posthog.com` or EU equivalent. |

- [ ] Billing limits set in PostHog (session replays are the usual first paid line item)
- [ ] App runs cleanly when these vars are absent (local dev without analytics)

## Sentry

| Variable | Secret? | Where to get it |
|---|---|---|
| `NEXT_PUBLIC_SENTRY_DSN` | No | Sentry, project settings, Client Keys (DSN). |
| `SENTRY_AUTH_TOKEN` | YES | Sentry, Auth Tokens. Build-time only, for source map upload. Vercel env, not `.env.local` in git. |

- [ ] Throw a deliberate test error in production and confirm it lands in Sentry with readable stack traces (source maps working)

## Final sweep

- [ ] `.env.example` matches reality: every var above listed with a placeholder and one-line comment
- [ ] `.env.local` is in `.gitignore` and `git status` shows no env file staged
- [ ] Vercel envs scoped correctly: preview deployments use test keys, production uses live keys
- [ ] Boot the app with one required var deliberately removed and confirm it crashes at startup naming the var (your zod env validation works)
- [ ] Grep the repo for `sk_live`, `whsec`, and `service_role`: zero hits outside `.env.local`
