teqo-backup/.agents/skills/local-database/SKILL.md
Francisco Solla 8125334401 feat(worktree): ambientes isolados por worktree — porta própria, bancos próprios, migrations no provision
pnpm worktree next [--go] [--no-migrate] agora provisiona um ambiente
determinístico por worktree: slot = parte numérica do código da Issue
(C15→15; hash sem dígitos; bump em colisão entre worktrees vivos), dev
server em 3100+slot, bancos teqo_wt<slot> / teqo_wt<slot>_test no
container compartilhado (-p teqo) com a cadeia de migrations aplicada;
escreve .env.local + .env.test.local (gitignored, marcador gerado; envs
manuais nunca sobrescritos) e copia PAYLOAD_SECRET/Blob/VAPID do main.
kill remove o worktree e dropa os bancos gerados (WITH FORCE, best-effort).

Suites leem .env.test.local depois de .env.test (vitest.setup /
playwright / db-doctor / gate-ci espelham o layering) e
assertTestDatabase aceita teqo_test ou teqo_<worktree>_test em localhost
só (fail-closed preservado). db:start/db:stop fixam -p teqo (um
container de qualquer diretório); max_connections 200→400. Sem Docker
(Cursor Cloud): degrada ao compartilhado com aviso.

Derivação pura em scripts/lib/worktree-env.mjs (unit-tested); fluxo
validado end-to-end: provision → migrate → 598/598 int no banco do
worktree → kill sem resíduo.
2026-08-08 11:33:07 -03:00

7.9 KiB

name description
local-database Set up, refresh, and safely use the Teqo local Postgres database, and copy production content locally with `pnpm db:pull`. Use when starting local development, when the local database is empty or stale, when preparing the test database, or when the user mentions the local DB, docker-compose, db:pull, or seeding local data. Enforces that local development and tests never touch the production Neon database.

Teqo Local Database

Production is a live Neon Postgres with real citizens' PII. Local development and tests run against a local Docker Postgres and must never connect to production.

Guardrails already in place (do not weaken them)

  • pnpm dev runs scripts/guard-dev-db.mjs and refuses to start if DATABASE_URL is a non-local host. Escape hatch: ALLOW_REMOTE_DB=true (use only with a very good reason).
  • Tests load .env.test (database teqo_test) and call assertTestDatabase() (tests/helpers/assertTestDatabase.ts), which throws unless the database name ends in _test.
  • Never repoint DATABASE_URL in .env, .env.local, or .env.test at Neon. .env.test is committed and local-only on purpose.

Cursor Cloud (sem Docker)

VMs Cloud não rodam docker compose / pnpm db:start. Postgres nativo: .cursor/environment.json (.cursor/cloud-setup.sh + .cursor/ensure-postgres.sh). Mesmos URLs localhost:5432/teqo e teqo_test. Agentes em work-issue / agent-work-issue: não tente Docker; gate:fast/gate:push não precisam de banco.

Local connection values

  • Dev database: postgresql://teqo:teqo@localhost:5432/teqo
  • Test database: postgresql://teqo:teqo@localhost:5432/teqo_test
  • Local Postgres major version is pinned to 17 in docker-compose.yml to match production Neon.
  • In a worktree provisioned by pnpm worktree next, read .env.local / .env.test.local instead: dev database teqo_wt<slot>, test database teqo_wt<slot>_test, dev server on port 3100+slot (see "Per-worktree environments" below).

Per-worktree environments (pnpm worktree next)

Parallel agents must not fight over port 3000, the shared teqo_test, or a second Postgres container. pnpm worktree next [--go] provisions each worktree with a deterministic isolated env:

  • slot = numeric part of the issue code (C15 → 15, B164 → 164; hash fallback without digits; bumped on collision among live worktrees).
  • Dev server port 3100+slot; databases teqo_wt<slot> (dev) and teqo_wt<slot>_test (tests), created on the one shared container with the full migration chain applied (one-time, ~30 s each).
  • Writes .env.local (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and .env.test.local (test DATABASE_URL). Both carry the marker # teqo worktree env (generated by pnpm worktree next); manual env files are never overwritten — remove them and re-run pnpm worktree next to regenerate.
  • --no-migrate skips the migration runs; pnpm worktree kill drops the worktree's generated databases (best-effort, DROP DATABASE … WITH (FORCE)).
  • Without Docker (Cursor Cloud) it degrades to the shared teqo/teqo_test with a warning and no port override.
  • Pure derivation: scripts/lib/worktree-env.mjs (unit-tested in tests/unit/worktree.unit.spec.ts).

First-time setup

  1. Ensure Docker is running (open -a Docker on macOS if the daemon is down).
  2. pnpm db:start — boots local Postgres and auto-creates teqo_test.
  3. Set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local.
  4. Load schema + data (pick one):
    • Mirror prod content (recommended): see "Refresh local content from prod" below.
    • Empty schema from migrations: pnpm migrate.
  5. pnpm dev.

Stop the database with pnpm db:stop.

Troubleshooting connectivity (pnpm db:doctor)

When pnpm dev, pnpm build, or the int suite cannot reach Postgres, run pnpm db:doctor before anything else. It checks the dev (.env.local/.env) and test (.env.test) targets with a real connection and prints the concrete remedy: which container to docker start, which foreign container is holding the port, or that a container is crash-looping (docker logs). pnpm dev runs the same preflight through guard-dev-db.

It also flags Postgres containers sharing one data volume. Never run a second Postgres container against teqo_pgdata: the postmaster lock cannot interlock across container PID namespaces, so both instances write the same WAL and corrupt it (this destroyed the local cluster's checkpoint on 2026-07-25 — recovered via pg_resetwal + rebuilding teqo_test). The canonical setup is ONE compose container (teqo-postgres-1, host port 5432) hosting both teqo and teqo_test.

Refresh local content from prod (pnpm db:pull)

PROD_DATABASE_URL="<unpooled Neon URL>" pnpm db:pull
  • The unpooled URL is DATABASE_URL_UNPOOLED in .env.local.
  • scripts/db-pull.mjs only reads prod (via pg_dump) and only writes local; it refuses any non-local target.
  • It excludes PII: row data of contact, signature, subscription (and their _rels) is not copied; the table structure is kept.
  • It resets and rebuilds the local public schema, so it overwrites local data.

Seed news content (pnpm db:seed:posts)

pnpm db:seed:posts
  • scripts/seed-posts.mjs fetches ~39 articles live from jorgesolla.com.br (WordPress REST API, HTML-crawl fallback) and imports them into post/tag/media as published noticia posts, converting the WP HTML body to Payload Lexical and uploading covers to Vercel Blob.

  • Same safety guard as pnpm dev: it refuses a non-local DATABASE_URL unless ALLOW_REMOTE_DB=true.

  • Idempotent by slug — re-running skips existing posts and reuses tags/media, so it never duplicates. Because the Vercel Blob store is shared across environments, it deletes any orphan blob under the deterministic <slug>.<ext> key before uploading, so it stays re-runnable.

  • Post-seed revalidation (required for prod): the seed writes from a CLI process outside the deployed runtime, so it does not bust production's frozen posts cache. After seeding prod content — or after ANY direct-DB write — bust it:

    curl -X POST "https://<prod-domain>/api/revalidate" -H "x-revalidate-secret: $REVALIDATE_SECRET"
    

    REVALIDATE_SECRET must be set in the Vercel production env. See the "Posts & Tags" section in AGENTS.md for the full rationale (returns 401 on a bad secret, 500 if the secret is unset).

Seed TSE 2022 election results (pnpm db:seed:tse)

pnpm db:seed:tse
TSE_CACHE_DIR=./data/tse pnpm db:seed:tse   # reuse downloaded zips
  • scripts/seed-tse-results.mjs downloads the TSE open-data zips for the 2022 general election (provenance URLs + SHA-256 in the script header), parses the Bahia scope, and imports into electionTally / electionCandidateVote / electionCandidate via a drizzle transaction.
  • Same safety guard as pnpm dev: it refuses a non-local DATABASE_URL unless ALLOW_REMOTE_DB=true.
  • Idempotent by scope — each (year, office, turn) scope is replaced wholesale (delete + bulk insert), so re-running never duplicates.
  • No revalidation step: campaign pages are dynamic with auth (no ISR tags). Downloads are cached under data/tse/ (gitignored).

Prepare / reset the test database

The test DB needs the schema applied via migrations:

DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate

Then run pnpm test (or pnpm test:int / pnpm test:e2e). Playwright injects the test DB into its own dev server and never reuses a running one.

Never do

  • Point any DATABASE_URL at Neon for dev or tests.
  • Run the test suite against a database not ending in _test.
  • Try to use db:pull to push local → prod (it cannot; it is read-only on the source).
  • Schema changes and migrations: use the payload-migrations skill.