diff --git a/control-plane/src/cutover.ts b/control-plane/src/cutover.ts index dd4658f..40086ba 100644 --- a/control-plane/src/cutover.ts +++ b/control-plane/src/cutover.ts @@ -153,6 +153,15 @@ export async function stageA(name: string): Promise { if (!row.db_password) { throw new Error(`project '${name}' has no stored db password — re-provision it`); } + // stageA assumes the app is serving from pg16: it (re)builds a fresh v17 copy + // from the current pg16 data. Refuse if the project is ALREADY on pg17 — its + // v17 db is live, and re-running would drop it and overwrite with a stale pg16 + // snapshot. Roll back to pg16 first if a re-run is really intended. + if (row.cluster === "pg17") { + throw new Error( + `project '${name}' is already bound to pg17 — stageA would overwrite its live v17 db with a stale pg16 copy. Roll back first (cutover --rollback) if you mean to re-run.`, + ); + } const password = row.db_password; const src = connOf(config.adminUrl); @@ -186,13 +195,15 @@ export async function stageA(name: string): Promise { `alter role ${ident(role)} set idle_in_transaction_session_timeout = '60s'`, ); - // 3. The database on v17 (from template0 — no supabase seed conflicts with - // the restore) + connect privileges scoped to this project's role only. - console.log(`[cutover ${name}] stageA 3/7 database ${database}`); - const d = await v17admin.query("select 1 from pg_database where datname = $1", [database]); - if (!d.rowCount) { - await v17admin.query(`create database ${ident(database)} template template0`); - } + // 3. The database on v17, ALWAYS from a clean slate (from template0 — no + // supabase seed conflicts with the restore). Dropping first makes stageA + // idempotent: a leftover copy (an earlier stageA, or a hand-staged dump) + // would otherwise make the pg_restore below fail on duplicate objects. The + // project is on pg16 here (guarded above), so nothing live depends on the + // v17 copy — WITH (FORCE) just clears any idle leftover connection. + console.log(`[cutover ${name}] stageA 3/7 (re)create database ${database} on pg17`); + await v17admin.query(`drop database if exists ${ident(database)} with (force)`); + await v17admin.query(`create database ${ident(database)} template template0`); await v17admin.query(`revoke connect on database ${ident(database)} from public`); await v17admin.query(`grant connect on database ${ident(database)} to ${ident(role)}`); } finally {