From 3d552557b7d01ad0ab55f6472ac7d1fcd19dfe2a Mon Sep 17 00:00:00 2001 From: Vitor Alves Date: Thu, 2 Jul 2026 22:26:21 +0000 Subject: [PATCH] feat(control-plane): per-tenant resource governance on the authenticator role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0007 (Phase 0) — bound one tenant's blast radius on the shared cluster. Every provisioned authenticator role now gets, idempotently: - connection limit 20 (peak real usage ~7, well under max_connections 100) - statement_timeout 60s - idle_in_transaction_session_timeout 60s GoTrue connects as `postgres` (not the authenticator) and migrations run as superuser, so this bounds the data plane without touching auth or schema apply. Existing 27 tenant roles were backfilled out of band; this makes new projects born-governed. Co-Authored-By: Claude Opus 4.8 --- control-plane/src/provision.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/control-plane/src/provision.ts b/control-plane/src/provision.ts index e0c1b72..fec3b0a 100644 --- a/control-plane/src/provision.ts +++ b/control-plane/src/provision.ts @@ -25,6 +25,23 @@ import { defaultOrgId } from "./orgs"; const SLUG = /^[a-z][a-z0-9_]{1,40}$/; +/** + * Per-tenant resource governance (ADR 0007, Phase 0) — bound one tenant's blast + * radius on the shared cluster. Applied to the authenticator role on every + * provision, so re-provisioning also backfills existing projects. + * - connection limit: a ceiling far above real peak (~7) yet well under the + * cluster's max_connections (100), so one runaway tenant can't drain the pool. + * - statement_timeout: kill a runaway query instead of pinning a backend. + * - idle_in_transaction_session_timeout: reap a leaked open transaction that + * would otherwise hold a connection (and its locks) open indefinitely. + * GoTrue connects as `postgres` (not the authenticator), so these bound the data + * plane without touching auth; migrations run as superuser, so statement_timeout + * never blocks a schema apply. + */ +const TENANT_CONNECTION_LIMIT = 20; +const TENANT_STATEMENT_TIMEOUT = "60s"; +const TENANT_IDLE_TX_TIMEOUT = "60s"; + export type Project = { name: string; database: string; @@ -103,6 +120,18 @@ export async function provisionDatabase(name: string): Promise { await admin.query(`alter role ${ident(role)} password ${lit(password)}`); } + // Per-tenant resource governance (ADR 0007, Phase 0). Idempotent; runs on + // every provision so existing roles get backfilled too. + await admin.query( + `alter role ${ident(role)} connection limit ${TENANT_CONNECTION_LIMIT}`, + ); + await admin.query( + `alter role ${ident(role)} set statement_timeout = ${lit(TENANT_STATEMENT_TIMEOUT)}`, + ); + await admin.query( + `alter role ${ident(role)} set idle_in_transaction_session_timeout = ${lit(TENANT_IDLE_TX_TIMEOUT)}`, + ); + const d = await admin.query( "select 1 from pg_database where datname = $1", [database],