Developer postgres

Start, stop and reset the developer Postgres cluster this project's tests run against.

markdownguidev1GNAM-WQDBpostgresrunbookdevelopmenttests

Updated 2026-10-17

Runbook: the developer Postgres cluster

Hand: agent. No root, no sudo, no pm2, no shared cluster. Item: FEAT-009, sprint S1. For the production cluster, which is operator-hand, see RUNBOOK-prod-postgres-cluster.md.

This cluster exists so an agent can build and test Part V's isolation model without touching anything shared. It is a second PostgreSQL 16 cluster, initialised into a directory this user owns, listening on loopback on a port nothing else uses.

What the host already provides

Verified on this host, 2026-10-11:

server binaries/usr/lib/postgresql/16/bin (PostgreSQL 16.13, Ubuntu 24.04 build)
pgvectorpresent, version 0.6.0 — vector.so and vector.control are installed
ltreepresent, version 1.2
pgcryptopresent, version 1.3
uuid-ossppresent, version 1.1 (not used: gen_random_uuid() is in core since 13)

initdb and pg_ctl both run as an unprivileged user, so no operator step is needed for the developer cluster. pgvector being already installed means item 5 of EPIC-PLAN.md §1 needs no package work either — only pg_createcluster, the roles and pgBackRest.

Commands

scripts/dev-pg.sh init      create the data directory, roles, extensions, two databases
scripts/dev-pg.sh start     start it
scripts/dev-pg.sh stop      stop it (fast shutdown)
scripts/dev-pg.sh status    cluster state, the role matrix, the extension list
scripts/dev-pg.sh reset     stop, delete the data directory, init again

init is run once. After a reboot, start.

What init builds

data directory<repo>/.dev-pg/data, gitignored, mode 0700
log<repo>/.dev-pg/postgres.log
port5440
socket/tmp/cuppy-dev-pg-<uid>
databasescuppy (development) and cuppy_test (the suite)
rolescuppy_app, cuppy_jobs, cuppy_migrate (login) and cuppy_rls (nologin)
superuserpostgres — named so because Z3 asserts rolname <> 'postgres' when counting rolbypassrls, and a differently named bootstrap superuser fails that check by construction
credentials<repo>/.dev-pg/dev-pg.env, mode 0600, gitignored. One DSN per role, for both databases
authlocal socket trust (the directory is 0700 and owned by this user); TCP scram-sha-256, so every loopback connection the application makes needs a password

Ports 5432 and 5433 are refused by name in the script: 5432 is the shared production cluster (datashield_gtm and friends), 5433 is reserved for the operator-provisioned cuppy production cluster (F3). Every other value is a variable with an environment override; see the head of scripts/dev-pg.sh.

The socket directory must stay short: the kernel caps a unix socket path at 107 bytes, and an agent scratch directory under /tmp/claude-*/… already exceeds it. The script checks the length and refuses rather than failing inside pg_ctl.

Applying the migrations

set -a; . .dev-pg/dev-pg.env; set +a
cd apps/api
npx tsx src/scripts/migrate.ts          # the production sequence
npx tsx src/scripts/migrate.ts --test   # plus the gate-test fixtures
npx tsx src/scripts/migrate.ts --list   # what has landed

The runner connects as cuppy_migrate and reads CUPPY_MIGRATE_DATABASE_URL. It never reads DATABASE_URL: the process environment pm2 hands this service pins that variable to a database belonging to another application, and CLAUDE.md rule 3 forbids using it. There is no fallback, so an unset CUPPY_DATABASE_URL is an unconfigured database rather than a default one.

Running the gate tests

Nothing to configure. apps/api/test/global-setup.ts probes the cluster once per run, applies both migration directories to cuppy_test, seeds the fixture graph, and writes a verdict to apps/api/.test-storage-mcp/pg-probe.json. With the cluster down the V3 gate suite skips and prints its reason as the name of a passing test, so the full suite stays green offline:

cd apps/api && npx vitest run
#   cluster up:   940 passed
#   cluster down: 893 passed | 48 skipped

Two safety interlocks, because the fixture seeder truncates every table:

  • loadDevPgEnv() builds the three DSNs config reads only from the CUPPY_TEST_*

set, and clears them when any is missing, so a partial env cannot leave the runner pointed at the development database.

  • seedFixture() reads current_database() and refuses to run unless the name ends in

_test.

Troubleshooting

symptomcause
could not start server, nothing in the logsocket path over 107 bytes. Set CUPPY_DEV_PG_SOCKET_DIR to something short
fe_sendauth: no password suppliedTCP is scram. Source .dev-pg/dev-pg.env, or connect over the socket with -h /tmp/cuppy-dev-pg-<uid>
port 5433 is reservedby design. 5433 belongs to the operator's cuppy cluster
role cuppy_app is absent from migration 001the cluster was initialised without scripts/db/bootstrap-roles.sql. Run reset
a migration fails with permission denied for table <one of the six>that table is owned by cuppy_rls; the statement must run under SET LOCAL ROLE cuppy_rls, or needs GRANT REFERENCES back to cuppy_migrate. Both patterns are in 005 and 006

What this cluster is not

It is not a staging environment and holds no real data. It is destroyed and rebuilt by reset without ceremony. The production cluster is provisioned once, by the operator, from the other runbook, and this file never touches it.

Read this document as markdown — the same bytes an agent receives.