Go-live runbook
Status of each launch item, and the exact remaining steps. Items marked (you) need your dashboards/accounts; (deploy) happen once the app has a public URL.
1. Integration keys
| Key | Status |
|---|---|
| Supabase URL + anon key | ✅ in .env.local |
Wise (key + profile 13147180) | ✅ verified (sandbox for dev; draft-only payouts) |
| Gmail (client/secret/refresh/inbox) | ✅ verified |
Hubstaff (PAT → 24h token, org 689363) | ✅ verified |
OpenAI (gpt-4o) | ✅ verified |
| OpenWeatherMap | ✅ verified (active) |
| Google SSO client id/secret | ✅ stored — (you) enter in Supabase → Auth → Providers → Google |
| DocuSeal API key | ⬜ (you) — docker run -d --name docuseal -p 3001:3000 -v docuseal_data:/data docuseal/docuseal, open http://localhost:3001, create admin, Settings → API → Generate, paste into .env.server DOCUSEAL_API_KEY (and the deployed env) |
SUPABASE_SERVICE_KEY | ⬜ (you) — Supabase → Project Settings → API → reveal service_role; needed server-side |
2. Database
- Migrations applied through
0026(latest: Wise contractor identifiers);src/db/migrations/is the source of truth, applied via Supabase tooling/MCP (ADR-0002). pg_cron/pg_netextensions — migration0019applied (extensions enabled). The cron jobs are still unscheduled — wire them in §4 after deploy (they need the app URL + secret).
3. Deploy (deploy)
- Pick a host (Vercel recommended for Next.js). Connect the repo; build =
pnpm build, Node 24 (set the Vercel project's Node.js version to 24.x so it matches.nvmrc/engines). - Frontend env:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY. - Server env (never
NEXT_PUBLIC_): everything in.env.server—SUPABASE_SERVICE_KEY,SUPABASE_PROJECT_REF,WISE_*,GMAIL_*,HUBSTAFF_*,DOCUSEAL_*,OPENWEATHERMAP_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY(fallback),ADMIN_SSO_ALLOWED_DOMAIN=nightingalepm.com,OPERATING_TIMEZONE(nowAmerica/New_York), and a freshCRON_SHARED_SECRET.Required at boot (validated by
src/server/env.ts, so the build/host fail fast if missing):NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY,SUPABASE_SERVICE_KEY,SUPABASE_PROJECT_REF,ADMIN_SSO_ALLOWED_DOMAIN. The rest are lazily checked per feature. - Google SSO redirect: in Google Cloud, the SSO OAuth client's redirect URI must be
https://dksjqoknmwpxfqkiygkb.supabase.co/auth/v1/callback; in Supabase → Auth → URL Configuration addhttps://<app-host>/api/auth/callback(+ keephttp://localhost:5173|3002). - DocuSeal: deploy DocuSeal on its own host with SSL; set
DOCUSEAL_BASE_URLto that URL.
4. Cron wiring (deploy) — run once in the Supabase SQL editor after deploy
create extension if not exists pg_cron; -- migration 0019
create extension if not exists pg_net;
-- Replace <APP_URL> with the deployed origin and <SECRET> with CRON_SHARED_SECRET
-- (prefer storing the secret in Supabase Vault rather than inlining it).
select cron.schedule('lock-ended-assignments', '10 6 * * *', $$
select net.http_post(
url := '<APP_URL>/api/cron/lock-ended',
headers := jsonb_build_object('Authorization', 'Bearer <SECRET>'));
$$);
select cron.schedule('accrue-overdue-interest', '20 6 * * *', $$
select net.http_post(
url := '<APP_URL>/api/cron/accrue-interest',
headers := jsonb_build_object('Authorization', 'Bearer <SECRET>'));
$$);
-- Wise payout reconciliation (reads Wise transfers, links them to prepared payments).
-- Needs WISE_API_KEY + WISE_PROFILE_ID set; runs every 2 hours.
select cron.schedule('wise-reconcile-payouts', '0 */2 * * *', $$
select net.http_post(
url := '<APP_URL>/api/cron/wise-reconcile',
headers := jsonb_build_object('Authorization', 'Bearer <SECRET>'));
$$);
-- Recruitment (only when RECRUITING_ENABLED): advance candidate nurture cadences (drafts emails —
-- never auto-sends) + release referral rewards at day-90. Daily.
select cron.schedule('recruiting-follow-ups', '30 6 * * *', $$
select net.http_post(
url := '<APP_URL>/api/cron/follow-ups',
headers := jsonb_build_object('Authorization', 'Bearer <SECRET>'));
$$);
-- Recruitment: RA 10173 retention purge — hard-delete expired soft-deleted non-hire candidates +
-- their documents + raw payloads. Daily.
select cron.schedule('recruiting-retention-purge', '40 6 * * *', $$
select net.http_post(
url := '<APP_URL>/api/cron/retention-purge',
headers := jsonb_build_object('Authorization', 'Bearer <SECRET>'));
$$);
Handlers fail closed: an unset/mismatched CRON_SHARED_SECRET returns 401.
5. Smoke test after deploy
- Admin Google SSO login →
/admin; contractor email/password →/contractor. - Generate an invoice; record a Wise sandbox payout (draft only); send a test Gmail.
- Punch in/out on the calendar; confirm a cron route returns 200 with the bearer secret.
6. Pre-launch hygiene
- Rotate the production Wise and OpenAI keys that were pasted in chat during setup.
- Keep
.env.serverout of git (already gitignored); load secrets via the host's secret manager.