Skip to main content

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

KeyStatus
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_net extensions — migration 0019 applied (extensions enabled). The cron jobs are still unscheduled — wire them in §4 after deploy (they need the app URL + secret).

3. Deploy (deploy)

  1. 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).
  2. Frontend env: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY.
  3. Server env (never NEXT_PUBLIC_): everything in .env.serverSUPABASE_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 (now America/New_York), and a fresh CRON_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.

  4. 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 add https://<app-host>/api/auth/callback (+ keep http://localhost:5173|3002).
  5. DocuSeal: deploy DocuSeal on its own host with SSL; set DOCUSEAL_BASE_URL to 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.server out of git (already gitignored); load secrets via the host's secret manager.