# Pre-prod checklist

Things to clean up / decide / verify **before** merging `dev` → `main`.
Project rule: this file must be empty when we ship.

Add an entry the moment you introduce debt, not later.

---

## Open

### Infrastructure

- [ ] **Multi-région obligatoire avant prod** : pour l'instant la stack sitespeed locale ne tourne que sur un seul nœud (FR). On veut tester depuis plusieurs régions (US, EU, APAC…). À décider :
  - 1 worker par région avec son propre Redis local, Postgres central répliqué
  - OU 1 worker central qui dispatche vers le bon nœud régional via une `region` dans le job
  - `SITESPEED_API_URL` devient un map `{region → url}` côté Next.js, ou un LB qui route selon la région demandée
  - Ajouter colonne `region` à `siteTests` pour pouvoir filtrer / comparer perf France vs US
  - Dimensionner chaque nœud d'après le stress test (voir `docs/stress-test-results.md`)
- [ ] **In-memory rate limit** (`lib/rate-limit.ts`) is single-instance only. If we go multi-instance in prod (PM2, Docker swarm, multiple Vercel-style functions), the limit becomes per-instance and effectively N× what we think it is. Swap for Redis (Upstash / managed) or row-based limit in Postgres before scaling out.
- [ ] **`docker-compose.yml`** ships a local Postgres for offline dev. Not used in prod (you brought an external dev DB). Either keep it documented as a fallback or delete.
- [ ] **`scripts/seed-test-users.ts`** hardcodes 3 test users (`owner-test`, `admin-test`, `member-test` @elevaseo.com) with `*Test!2026` passwords. The script now **refuses to run with `NODE_ENV=production`** (guard added 2026-05-26) so it can no longer accidentally seed those accounts on a prod DB, but the accounts still need to be removed from prod if they got created during testing.

### Env / secrets

- [ ] **`SITESPEED_API_KEY` — ROTATE before prod.** La valeur actuelle a été exposée en clair dans une conversation Claude Code (transcript persistant) le 2026-05-24 = à considérer comme compromise. Générer une nouvelle clé (`openssl rand -hex 32` ou format `sk-speed…` côté API), la mettre simultanément dans le `.env` serveur (à côté du `docker-compose.yaml` de `speed.elevaseo.app`) **et** dans `.env.local` / variables d'env de l'app Next.js, puis `docker compose up -d` côté upstream pour rechargement.
- [ ] **`BETTER_AUTH_SECRET` — vérifier exposition.** Même session de debug que ci-dessus a fait des `grep` sur `.env.local`. La sortie a masqué les valeurs (`<REDACTED>`) mais par prudence, vérifier qu'aucune trace de la valeur réelle n'est apparue en clair avant de prod-iser, sinon rotation aussi.

### Lead capture rules (cf. `to-think.md`)

- [ ] **Consumer domain blocklist** is also a hand-picked list. Acceptable for now; revisit if we get reports of false positives.
- [ ] **2nd-audit dedupe key** is currently **hostname-only, forever** (cf. integration API). Open question parked in `docs/planning/to-think.md` — confirm this is the desired UX or refine (cookie+hostname, time window, etc.).

### Code shape

- [ ] **`databaseHooks.user.create.after`** inserts a `leads` row on every signup with `consent: false`. Need to align with marketing/legal on whether signup implies marketing consent or stays separate (opt-in checkbox in signup form?).
- [ ] **Bastien is in `user` but not in `leads`** — he signed up before the hook existed. Backfill if we treat the admin as a lead, or leave him out (probably leave out).

### Stripe webhook (dev)

To exercise the webhook handler in dev:

1. Install Stripe CLI: `brew install stripe/stripe-cli/stripe` (or follow https://docs.stripe.com/stripe-cli).
2. Authenticate once: `stripe login`.
3. Forward webhooks to local:
   ```bash
   stripe listen --forward-to localhost:3000/api/stripe/webhook
   ```
   The CLI prints `Ready! Your webhook signing secret is whsec_…` — paste this into `.env.local` as `STRIPE_WEBHOOK_SECRET`.
4. Trigger events for testing:
   ```bash
   stripe trigger checkout.session.completed
   stripe trigger customer.subscription.updated
   stripe trigger invoice.payment_failed
   ```
5. Use test cards on Checkout:
   - `4242 4242 4242 4242` — success
   - `4000 0000 0000 0341` — 3DS required
   - `4000 0000 0000 9995` — decline

### Cron / scheduled audits

- [ ] **`CRON_SECRET`** must be set in prod before enabling pg_cron / host cron. The `/api/cron/run-audits` endpoint returns 503 without it (safe default — no work happens) but if you wire pg_cron without setting the secret you'll get a stream of 503s in `cron.job_run_details`.
- [ ] **pg_cron + pg_net availability** on prod Postgres — confirm with the DB provider before relying on Option A in `cron-setup.md`. Fall back to host cron (Option B) if needed.

---

## Done

- [x] **Local SQLite leftovers** — `eleva-speed.db`, `*-shm`, `*-wal` removed from working tree (gitignored, no references remain). Resolved in `feat/preprod-cleanup`.
- [x] **`.vercelignore` baseline** — file already present at repo root with correct baseline (covers `docs/`, `.claude`, `.playwright-mcp`, `.knip`, `skills-lock.json`); checklist item was stale.
- [x] **`invitation.inviterId` FK blocks user self-deletion** — changed to `onDelete: "cascade"` in `lib/db/schema.ts` (commit `8fd1e8858`).
- [x] **No tier-based audit cap on cron** — `/api/cron/run-audits` now calls `checkQuota(tests)` per org and skips when exhausted (commit `8fd1e8858`).
- [x] **`BETTER_AUTH_SECRET` ≥32 chars in prod** — `lib/env.ts` Zod refine now enforces required + min(32) when `NODE_ENV === "production"` (commit `8fd1e8858`).
- [x] **`ELEVASPEED_INTEGRATION_API_KEY` required in prod** — `lib/env.ts` superRefine guards production, dev stays permissive (commit `8fd1e8858`; renamed from generic `INTEGRATION_API_KEY` to match the prefixed name used by elevaseo.com vitrine on Vercel).
- [x] **`invitation` table introduite Phase 1 mais non utilisée** — STALE. The table is actively used by the Phase 4 invitation flow (Better Auth `organization` plugin); not a knip false-positive anymore.
- [x] **Sidebar Alerts → 404** — nav item hidden until `/dashboard/alerts` page is built (commit `818e217ff`).
- [x] **Sitespeed launch logic duplication** — `/api/sites/route.ts`, `/api/sites/[siteId]/test/route.ts`, and `/api/integrations/audit/route.ts` now all delegate to `lib/sitespeed-launch.ts#launchSitespeedTest` (commit `fa019fbbc`).
- [x] **Disposable email blocklist** — swapped the hand-picked ~25-entry Set for the maintained `disposable-email-domains` npm package (~5k domains). Ambient type declaration in `lib/email-validation.d.ts` since the package ships no types. Resolved in `feat/preprod-cleanup`.
- [x] **Format debt — oxfmt** — `pnpm format` pass cleared all residual files (was 52, then 5, now 0). `pnpm format:check` is green across all 270 files. Resolved in `feat/preprod-cleanup`.
- [x] **Target hosting** — résolu par la stack Docker self-hosted introduite 2026-05-26 (`docker-compose.prod.yml`, `Dockerfile`, `services/egress-proxy/*`). Documentée dans `docs/infra/prod-deploy.md`.
- [x] **Migrations strategy** — documentée dans `docs/infra/prod-deploy.md` §3.4 : `docker compose exec app drizzle-kit migrate` lancé manuellement après chaque déploiement.
- [x] **`RESEND_FROM_EMAIL` set sur env prod** — listée dans `.env.prod.example`, rappelée dans `docs/infra/prod-deploy.md` §3.1. La valeur réelle est dans `.env.prod` (gitignored).
- [x] **`BETTER_AUTH_URL` set sur env prod** — idem (`.env.prod.example` + doc).
- [x] **`CRON_SECRET` set sur env prod** — idem (`.env.prod.example` + Zod schema enforce ≥32 chars en prod).
- [x] **SSRF protection sur `/api/speed/test` + `lib/url.ts`** — la route exigeait pas d'auth et `isReachable` faisait `redirect: "follow"` sans filtre. Fix 2026-05-26 : auth+rate-limit sur la route, et `lib/url.ts` bloque maintenant les hostnames qui résolvent vers RFC1918 / loopback / link-local / IMDS, redirects suivis manuellement avec re-validation à chaque hop.
- [x] **Egress proxy effectivement appliqué à Node `fetch`** — `HTTPS_PROXY` / `HTTP_PROXY` étaient set sur l'`app` container mais Node ne les respecte pas par défaut (undici). Fix 2026-05-26 : ajout de `NODE_USE_ENV_PROXY: "1"` sur les services `app` et `sitespeed-worker` dans `docker-compose.prod.yml`.
- [x] **Seed scripts refusent `NODE_ENV=production`** — `scripts/seed-user.ts` et `scripts/seed-test-users.ts` exit avec un message d'erreur si lancés en prod (fix 2026-05-26).
