Skip to content

Local CMS Development Stack

The supported local runtime is a Docker Compose stack with the CMS web app, PostgreSQL with pgvector, pgAdmin, and the RAG and presentation workers. External S3, AI, email, billing, Python, and Unity services remain outside this stack.

Prerequisites

  • Docker Desktop with Docker Compose v2
  • enough Docker resources for the Node development server and PostgreSQL
  • repository provider configuration in the ignored .env file when exercising flows that call external services

Configure

Create the ignored Compose environment file:

Copy-Item .env.compose.example .env.compose

Replace every CHANGE_ME value with a strong machine-local value. Do not commit .env.compose. The file supplies Postgres and pgAdmin credentials plus local runtime overrides. The optional ignored .env continues to supply provider, storage, email, and billing configuration.

Inside Compose, web connects to postgres:5432. Host-run tools use 127.0.0.1:5432. A service running on the Docker Desktop host must be addressed from the web container as host.docker.internal, not localhost.

Start and inspect

pnpm stack:config
pnpm stack:up
pnpm stack:status

pnpm stack:up runs docker compose up --build --wait; it builds the web image, starts all five services, and waits for the health-gated services. The RAG and presentation workers start only after web is healthy, use the internal postgres:5432 connection, and keep Payload schema push disabled.

The web launcher waits for PostgreSQL and inspects the machine-local Payload schema before starting Next.js. With PAYLOAD_DB_PUSH=1, it keeps schema push enabled for an empty first boot and disables it when the Payload tables already exist, making routine restarts safe without changing hosted-database policy.

Endpoints:

  • CMS and Payload admin: http://127.0.0.1:3000 and http://127.0.0.1:3000/admin
  • PostgreSQL: 127.0.0.1:5432
  • pgAdmin: http://127.0.0.1:5050

pgAdmin has a preconfigured postgres:5432 server entry. It does not persist a database password in the tracked server definition.

The queue workers run as rag-worker and presentation-worker. Inspect or follow their logs independently:

docker compose --env-file .env.compose logs --tail 200 rag-worker presentation-worker
docker compose --env-file .env.compose logs --follow rag-worker presentation-worker

Lifecycle and persistence

pnpm stack:logs
pnpm stack:restart
pnpm stack:down
pnpm stack:up

Routine restart and stack:down preserve the Postgres, pgAdmin, dependency, and Next.js cache named volumes. Do not add --volumes to normal stop commands.

The destructive reset is a separate guarded command. Its default mode prints the exact project and volume targets without changing them:

pnpm stack:reset

After reviewing that plan and obtaining approval, execution requires the exact project confirmation:

pnpm stack:reset -- --execute --confirm anuvax-cms-local

This deletes only the named volumes in the printed local Compose project. It is not a database rollback and must never be used against an unverified project.

Pinned baselines

The tracked definitions use immutable multi-architecture digests resolved from the official Docker Hub tag API on 2026-07-21.

Runtime Pinned reference Multi-architecture digest Linux amd64 child digest
Web base node:22.5.1-bookworm-slim sha256:2fb92fe9d7350866a73c5cc311c1a19919ffd47e8592d4233374ee330e3bdb1e sha256:ec92a9eaa01e8e098dc7da1c884a3d2a0f10c66b6dac6b1ca054cc5908442ab3
Postgres and pgvector pgvector/pgvector:0.8.2-pg17-bookworm sha256:feb68f4f15446397d8cac7f4fe48fe4586de83160d1fc48b46283312d1a33966 sha256:e04af45eb526378554a24ed05b37d9ea56fd623feca9adf264d4f47d875c9a93
pgAdmin dpage/pgadmin4:9.16 sha256:40fa840c5bb7c8463957f1255b01283732c2d8c9396a956d180f8e6c296753b3 sha256:66a300a7ecdcc1f325af0c430315329bca46cd4a7067227d6899802238167c6e

The required running versions are PostgreSQL 17.10, pgvector 0.8.2, pgAdmin 9.16, and Node 22.5.1. Verify them from the running containers rather than inferring them only from tags:

docker compose --env-file .env.compose exec -T postgres postgres --version
docker compose --env-file .env.compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT extversion FROM pg_extension WHERE extname = ''vector'';"'
docker compose --env-file .env.compose exec -T pgadmin /venv/bin/python -c "import config; print(config.APP_VERSION)"
docker compose --env-file .env.compose exec -T web node --version

Application smoke

pnpm stack:verify

The smoke initializes Payload and Mastra storage, performs a deterministic three-dimensional pgvector insert/query/delete cycle, and calls the authenticated worker jobs endpoint. It disables schema push in its separate process, deletes its temporary vector index, closes owned storage pools, and exits on completion.

Database migration and cutover runbook

ANU-7 completed the first accepted Supabase-to-local cutover and hardened the migration tooling for repeatable recovery or refreshes. The Supabase CLI is pinned as a project dependency. Commands default to non-connecting plan mode, read connection strings only from named environment variables, and allow artifacts only below ignored .local-data/database/<run-id> storage.

Use pnpm local-stack:db:env for source-only operations loaded from the ignored .env. Use pnpm local-stack:db:migrate when a command needs both the source and a loopback target derived from .env.compose. Set ANUVA_LOCAL_DATABASE to select a fresh target database without changing tracked configuration. The create-target guard derives its administrative connection by replacing that not-yet-created database name with the always-present local postgres database; host, port, user, password, and query parameters remain unchanged.

pnpm local-stack:db:env -- inventory --database-env DATABASE_URI --artifact-dir .local-data/database/<inventory-run> --label source
pnpm local-stack:db:env -- export --source-env DATABASE_URI --artifact-dir .local-data/database/<export-run> --schemas public
pnpm local-stack:db -- verify --artifact-dir .local-data/database/<export-run>
pnpm local-stack:db -- prepare --artifact-dir .local-data/database/<export-run>

$env:ANUVA_LOCAL_DATABASE = '<fresh-database>'
pnpm local-stack:db:migrate -- create-target --maintenance-env LOCAL_DATABASE_URI --database <fresh-database> --confirm-create <fresh-database>
pnpm local-stack:db:migrate -- restore --target-env LOCAL_DATABASE_URI --artifact-dir .local-data/database/<export-run> --confirm-target 127.0.0.1:5432/<fresh-database>
pnpm local-stack:db:migrate -- parity --source-env DATABASE_URI --target-env LOCAL_DATABASE_URI --artifact-dir .local-data/database/<parity-run>
pnpm local-stack:db:migrate -- probe --target-env LOCAL_DATABASE_URI --artifact-dir .local-data/database/<probe-run>

Review each plan before repeating it with --dry-run or --execute as the operation requires. The accepted sequence is:

  1. Run source inventory inside a read-only transaction. Confirm that only reviewed application schemas will be exported.
  2. Run the Supabase CLI dump dry run, then export schema and data. Do not export hosted roles unless a new review explicitly requires them.
  3. Verify the SHA-256 manifest and prove every artifact is ignored and absent from Git.
  4. Run prepare to create schema.local.sql. It removes only hosted ownership, grants, revokes, default privileges, and session authorization; it refuses managed Supabase schema data or residual hosted-role references.
  5. Create a fresh loopback target. Never restore over the accepted local database by default. Restore re-verifies checksums, passes dump paths as positional arguments rather than shell source, and uses one transaction.
  6. Require semantic parity and the targeted workspace, Payload, Mastra, RAG, queue, and vector probe before changing .env.compose POSTGRES_DB. The parity command writes its ignored report and exits nonzero on any mismatch.
  7. Recreate the Compose services with pnpm stack:up, then run migration status, pnpm stack:verify, HTTP/admin/login probes, worker startup probes, restart, and routine down/up persistence checks.

The align-target operation exists only for an empty target created with pgvector in a non-public schema. It refuses non-empty databases. New targets install pgvector under public, matching the portable Supabase CLI dump.

Recovery

  • Inventory, parity, and probe SQL run read-only. Export reads the source; it never writes to it.
  • A failed restore is rolled back as one transaction. Inspect the ignored restore log, correct only the reviewed compatibility difference, and retry against the still-empty target.
  • pnpm stack:down preserves local state for diagnosis.
  • Inspect pnpm stack:logs and the ignored database run directory before any clean retry.
  • Keep the original Supabase URI unchanged in ignored .env. To roll back, stop local application writes, run the CMS from .env with PAYLOAD_DB_PUSH=0, verify pnpm payload migrate:status, and leave the local Compose volume intact. Return forward by starting Compose with the accepted local POSTGRES_DB.
  • Never use broad Docker cleanup commands as recovery for this stack.