Skip to main content

Deployment

client-os targets three platforms: Render for the API, Vercel for the web app, and Supabase Cloud for the database and auth. This page summarizes what's involved — for the full, authoritative detail, read the two source documents directly:

The core gotcha: a git push alone doesn't bring prod up

Neither Render nor Vercel run this repo's @cogs/config file-loader (server-preload.cjs) at runtime. That means every environment variable the app reads — not just secrets, but the whole type: 'public' set too — has to be entered directly into each platform's own environment settings. Committed .env.prod files in this repo are not read on either platform; they exist for local reference only.

First deploy, in order

  1. Supabase Cloud — confirm the real project ref, grab the anon key and JWKS/issuer URLs, and confirm email confirmations are ON (local dev turns them off for frictionless testing; that setting doesn't travel to the cloud project). This repo has no custom access-token hook and no RLS — org-scoping is enforced entirely at the app layer (apps/api/src/auth/middleware.ts), so the usual "register the access-token hook" step doesn't apply here.
  2. Migrate the prod databasepnpm --filter @client/db db:deploy against the Supabase session pooler (:5432, sslmode=require; the transaction pooler on :6543 isn't reachable from Render's egress). Never run the demo seed scripts against a cloud connection.
  3. Render (API) — set ENV=prod, NODE_ENV=production, and every var the API's runtime actually reads (see render.yaml's annotated list). Never set AUTH_DEV_SECRET or SUPABASE_SERVICE_ROLE_KEY — neither is read by the API's production code path, and the former is explicitly rejected under NODE_ENV=production.
  4. Vercel (web) — set ENV=prod, NODE_ENV=production, API_URL (the live Render URL, not a localhost leftover), and the Supabase public URL/key, all in the dashboard's Production environment variables.
  5. Verify — API health check, / and /login render, and a full login round-trip confirming GET /api/me returns a non-empty roles array (this repo's equivalent of "the JWT carries org context").

Operating the live API

docs/RENDER-RUNBOOK.md is the day-2 reference once the API is live: service identity and dashboard links, the confirmed DATABASE_URL pooler host (watch for aws-1 vs the correct aws-0 — a real incident that caused every authenticated route to 500), why a plain Render /restart doesn't pick up new env vars (only a real deploy does), and a post-deploy verification checklist including a reminder that Vercel env var changes require an actual rebuild — reading a correct value back from the Vercel API is not proof the live page is serving it. Always curl the live payload to confirm.

Automation

scripts/deploy-env.mjs dry-runs (and, with --apply, executes) pushing the per-app .env.prod.local values to both platforms:

node scripts/deploy-env.mjs --stage prod # dry-run, prints the plan
node scripts/deploy-env.mjs --stage prod --apply # writes to both platforms

The database migration and the deploy trigger itself are intentionally left as manual, deliberate human actions — they're not scripted.