From c312e37b4a1ad21cdbc348f9e9e9add40e66d216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Tue, 20 Oct 2026 10:24:19 +0000 Subject: [PATCH] Drop the pooler from the MVP: point everything straight at Postgres .env.example and the README still told people to use pgCat on :6432, the exact setup that hangs on every parameterised query. With no prod and one backend instance there is nothing for a pooler to multiplex, and postgres-js already pools in-process, so DATABASE_URL now points at Postgres on :5432 in .env.example, dev-env.sh and drizzle.config.ts. prepare:false is kept in src/db/index.ts on purpose, so adding PgBouncer later (tested working) is a config change rather than a code change. The long measurement narrative in that file's comment is cut to the decision and the pointer to PLAN.md, where the numbers now live. --- .env.example | 4 ++-- README.md | 5 ++--- docker-compose.yml | 5 ++--- drizzle.config.ts | 7 +++---- scripts/dev-env.sh | 7 +++---- src/db/index.ts | 29 +++++++++-------------------- 6 files changed, 21 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 969f9e4..7c2394a 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ PORT=3000 -# Postgres, routed through pgCat/PgBouncer -DATABASE_URL=postgres://continuum:continuum@localhost:6432/continuum +# Postgres, directly - no pooler for the MVP (see src/db/index.ts) +DATABASE_URL=postgres://continuum:continuum@localhost:5432/continuum DATABASE_POOL_MAX=10 # Valkey (state + streams), NOT Redis - Redis 7.4+ is RSALv2/SSPL and 8.0+ diff --git a/README.md b/README.md index e0ffc8b..d897399 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # continuum-backend -Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (via pgCat/PgBouncer, -with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage. +Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage. ## Run @@ -48,7 +47,7 @@ backend and records what a gateway sends, for refreshing the fixtures in ## Layout - `src/db/` - Drizzle schema (`printers`, `farms`, `print_jobs`, `frame_embeddings`) - and client, pointed at pgCat. `src/db/validators.ts` derives Elysia/TypeBox + and client, pointed straight at Postgres. `src/db/validators.ts` derives Elysia/TypeBox request validators straight from the Drizzle tables via `drizzle-typebox`, so the Postgres schema and the API validators come from one definition instead of two hand-synced ones. diff --git a/docker-compose.yml b/docker-compose.yml index f37c356..489af28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,8 @@ # (`ioredis` here, the `redis` crate in continuum-ai-worker) are unchanged. # See continuum-common/docs/LICENSES.md. # -# pgCat/PgBouncer is deliberately not here - DATABASE_URL in .env.example -# points at :6432 for production, but talking straight to Postgres on :5432 -# is one less moving part locally. +# No pooler anywhere for the MVP: DATABASE_URL points straight at Postgres on +# :5432 here and in .env.example. See src/db/index.ts for why not pgCat. services: valkey: diff --git a/drizzle.config.ts b/drizzle.config.ts index e21e9c2..e05db01 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -5,10 +5,9 @@ export default defineConfig({ out: "./drizzle", dialect: "postgresql", dbCredentials: { - // Matches scripts/dev-env.sh's DEV_DATABASE_URL. It pointed at 6432 (a - // pooler) where nothing listens in this environment, so a bare db:migrate - // failed confusingly. Migrations want Postgres directly in any case: see - // the note in src/db/index.ts about parameterised queries through pgCat. + // Matches scripts/dev-env.sh's DEV_DATABASE_URL and .env.example: straight + // at Postgres. Migrations must never go through a pooler - drizzle-kit + // cannot pass `prepare: false`. url: process.env.DATABASE_URL ?? "postgres://continuum:continuum@localhost:5432/continuum", }, verbose: true, diff --git a/scripts/dev-env.sh b/scripts/dev-env.sh index 1d8f55e..a81ad84 100755 --- a/scripts/dev-env.sh +++ b/scripts/dev-env.sh @@ -35,10 +35,9 @@ cd "$REPO_ROOT" PG_VERSION=16 PG_PORT=5432 VALKEY_PORT=6379 -# Straight at Postgres, not pgCat: .env.example points at the pooler for -# production, and neither backend here runs one. Keep it that way even where a -# pooler is reachable - parameterised queries from postgres-js hang through -# pgCat 1.2.0, and drizzle-kit cannot migrate through it. See src/db/index.ts. +# Straight at Postgres: there is no pooler for the MVP. Do not point this at +# pgCat even where one is reachable - parameterised queries from postgres-js +# hang through 1.2.0. See src/db/index.ts. DEV_DATABASE_URL="postgres://continuum:continuum@localhost:${PG_PORT}/continuum" DEV_VALKEY_URL="redis://localhost:${VALKEY_PORT}" diff --git a/src/db/index.ts b/src/db/index.ts index 5049767..17a51b3 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,28 +2,17 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; -// `prepare: false` because this is meant to sit behind a transaction-mode -// pooler, where a named statement prepared on one server connection is not -// there on the next one. `max` is low for the same reason: the pooler -// multiplexes, so a high per-instance count buys nothing and starves peers. +// No pooler in front of this for the MVP: DATABASE_URL points straight at +// Postgres, and postgres-js already pools connections in-process (`max`). // -// Measured 2026-10-20, and the reason to read this before assuming the -// deployment works: against pgCat 1.2.0 this client cannot run at all. -// Through that pooler an unparameterised `select 1` succeeds, and so does -// `select count(*) from organisations`, but *any* query carrying a bind -// parameter hangs forever - with `prepare: false`, with `prepare: true`, with -// `fetch_types: false`, and inside an explicit transaction alike. The same -// queries straight at Postgres are fine, and psql's `\bind` through the same -// pool is fine, so it is specific to how postgres-js frames those messages, -// not to extended protocol in general. Every Drizzle query binds parameters, -// so there is no subset of this app that works there. +// `prepare: false` is kept deliberately so a transaction-mode pooler can be +// added later without touching this file. Which one matters - measured +// 2026-10-20: through pgCat 1.2.0 any query with a bind parameter hangs +// forever (every Drizzle query has one), while PgBouncer 1.22 in transaction +// mode runs the same queries and passes scripts/e2e-authz.sh. See PLAN.md. // -// So `prepare: false` is necessary but nowhere near sufficient, and running -// behind pgCat does not work. PgBouncer 1.22 in transaction mode does: the -// same queries pass, 40 concurrent ones multiplex over a pool of 2 server -// connections, and scripts/e2e-authz.sh passes all 68 checks through it. -// Migrations go straight at Postgres regardless - drizzle.config.ts has no way to pass -// `prepare: false`, and drizzle-kit hung at commit for the same reason. +// Migrations go straight at Postgres either way: drizzle.config.ts cannot +// pass `prepare: false`, and drizzle-kit hung at commit through pgCat. const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error("DATABASE_URL is not set");