prod-deploy
Why It Exists
The skill was extracted directly from a real deploy: the introducing commit
(32d31d9, "add prod-deploy skill") states it was "extracted from workloom,"
and its first 8 invariants encode what that deploy actually hit — including
that Render's egress can't reach the transaction pooler on :6543, only the
session pooler on :5432 with sslmode=require. That wasn't theoretical
either: the follow-up fleet rollout (docs/sessions/2026-08-05-fleet-migration-progress/SUMMARY.md)
records the skill being re-numbered specifically to add "Render :6543→:5432
WEBHOOK_SIGNING_KEY" after hitting the same landmine again on a second repo.
Invariants 9-14 came later, from a 13-repo rollout of this same pattern (the
enrichment commit e3f255f is explicit about the source). The most serious
finding from that rollout is invariant 12's routine rowsecurity=false sweep,
which the commit message says directly "caught a real live P0 — unauthenticated
cross-tenant CRUD+TRUNCATE" on a table created after the repo's own
"enable RLS on everything" migration had already run — proof that a
one-time RLS migration doesn't retroactively cover tables added afterward.
That sweep runs for free alongside the migration-drift check the skill was
already telling you to do (invariant 11), which is why it survived into the
skill as a standing invariant instead of a one-off incident note. It was
serious enough that its canary-row proof methodology — insert a row via the
privileged connection, confirm it's invisible to the restricted role, then
delete it — was later generalized and pointed to from an unrelated planning
document (.omc/plans/quality-loop-skills.md) as the reusable test-coverage
pattern for "every new RLS policy," rather than being re-derived from scratch.
The fleet rollout also surfaced a structural gap the skill doesn't fully close
yet: docs/sessions/2026-08-05-fleet-migration-progress/FOLLOWUPS.md flags
that repos on NextAuth/OIDC (rather than Supabase Auth) get org/role from an
OIDC claim instead of the access-token hook this skill assumes, making
invariant 5's "register the hook" step moot for that auth path — a documented,
still-open branch point rather than something already handled by a flag.
What It Does
prod-deploy is the production-deploy checklist and reference set for an
ENV-selected stack where the api runs on Render, the web app on Vercel, and
the database on Supabase Cloud. It exists because none of the manual, one-time,
per-environment setup — entering secrets and non-secret vars into each
platform's env UI, running the first cloud DB migration by hand, registering
the Supabase access-token hook and captcha secret in the dashboard, and
getting the pooler connection string right — happens automatically on git push. It ships a genericized PROD-DEPLOY.md checklist, an annotated
render.yaml, and a dry-run-by-default deploy-env.mjs that can script the
env-var setup and db:deploy (never the deploy trigger itself).
It complements runtime-env-config (the app-side env loader/EnvProvider
chain) and local-dev-stack (the same stack's local, not-prod, operational
half) — this skill is specifically the "make it work in prod, the first time"
half of that arc.
How To Use It
Triggers on: "first production deploy", "deploy to Render", "deploy to Vercel", "Supabase Cloud prod", "render.yaml", "ENV=prod not set / deployed but misconfigured", "ECONNREFUSED on Render :6543", "access-token hook not registered", "empty org/role in prod JWT", "set prod env vars on the platform", "migrate prod DB", "db:deploy", or standing up a first prod deploy.
skills add git@github.com:catesandrew/next-starters.git --skill skills/prod-deploy -g
npm install @next-starters/skill-prod-deploy
/plugin marketplace add catesandrew/next-starters
/plugin install prod-deploy@next-starters
Gotchas & Invariants
.env.<stage>never reaches either platform — Render's image.dockerignores.env.*and Vercel never runsserver-preload.cjs— so both the secrets and the "non-secret"type:'public'set (ENV, URLs, selectors) must be entered in the platform env, not just the secrets.ENV=<stage>is the stage selector, distinct fromNODE_ENV. Miss it and the loader falls back to base.env, so the deploy looks green but behaves like dev — the single most common "deployed but misconfigured" bug.- There is no migrate-on-deploy:
db:deploy(db:migrate && db:post) runs by hand against the cloud direct/session URL, schema first, then RLS/the auth-hook function — running that order backwards fails against tables that don't exist yet. config.tomldoesn't carry to Supabase Cloud. The access-token hook, captcha secret, and "email confirmations ON" are dashboard-only settings — the hook function can exist (created bydb:post) while prod JWTs still carry noorg_id, because the cloud was never told to call it.- On Render, use the session pooler
:5432+sslmode=requirefor both connection strings — Render's egress can't reach the transaction pooler:6543, and getting this wrong is anECONNREFUSEDon first boot, not a build failure. - A bare
pg.Poolwith nossloption andsslmode=requirethrowsSELF_SIGNED_CERT_IN_CHAINon first query;postgres.jsdoesn't have this trap. Never "fix" a live, already-working connection string to match this rule — a working value beats a theoretically-correct one. - Check migration drift before running
db:deployagainst any pre-existing DB: Drizzle's journal compares timestamps, not hashes, so schema applied out-of-band looks like drift. If the objects already exist, baseline the ledger rows instead of re-running the migration. - Sweep for
rowsecurity=falseevery time you check drift — it's nearly free and once caught a real live P0 (unauthenticated cross-tenant CRUD+TRUNCATE) on a table added after the repo's blanket RLS migration. - Vercel's bulk
env?decrypt=truelist endpoint can return ~1000-char garbage for any var regardless of type — always re-verify via the single-var endpoint before concluding a stored secret is corrupted. Also watch forignoreCommandusing a bareHEAD^, which silently skips a batched multi-commit push. - If org/role resolution is a per-request DB lookup rather than a JWT claim, check for the JIT-provisioning onboarding gap: a fresh zero-membership signup can get floored to a phantom non-empty role by an auth-fallback helper duplicated in more than one place in the same codebase.
Related Skills
- runtime-env-config — the app-side
loader/
EnvProviderchain this skill's platform env entries are read by at runtime. - local-dev-stack — the local counterpart:
the same
ENV-staged stack, process-composed and migrated locally instead of by hand against Supabase Cloud.
Sourced from: skills/prod-deploy/metadata.json, skills/prod-deploy/SKILL.md, git log --follow -- skills/prod-deploy (commits 32d31d9, e3f255f), docs/sessions/2026-08-05-fleet-migration-progress/SUMMARY.md, docs/sessions/2026-08-05-fleet-migration-progress/FOLLOWUPS.md, .omc/plans/quality-loop-skills.md (local research artifact, not tracked in this repo)