Skip to content

Codex handles database work on a Drizzle and PostgreSQL stack as a pipeline: it designs the schema, generates forward-only migrations with drizzle-kit, writes queries against the indexes that exist, and proves every change on a seeded database in a worktree or a Codex cloud environment. Humans review the migration SQL and the evidence, not every generated line.

Your product manager wants a reporting dashboard that aggregates orders, revenue, and refunds per user by day, week, and month. The orders table already has tens of millions of rows, the current queries are slow, and one blocking CREATE INDEX during business hours would take checkout down. This page is for the developer who owns that change in Codex, and for the tech lead who has to decide what “proven safe” means before it reaches production.

What you’ll walk away with from Codex database work

Section titled “What you’ll walk away with from Codex database work”
  • An AGENTS.md block that stops Codex from editing applied migrations or touching anything but a local or test database.
  • Five copy-paste prompts: schema design, migration, queries with tests, a cloud proof run, and a weekly query audit.
  • A Codex cloud environment setup script that gives each task its own PostgreSQL.
  • A read-only PostgreSQL MCP setup so Codex can read real query plans.
  • A verification stack (migration lint, apply-from-zero, differential query tests, and plan assertions) and a clear line on what a human still reads.

Which Codex surface fits each database task?

Section titled “Which Codex surface fits each database task?”

Database work splits cleanly across Codex’s surfaces. Pick by what the task has to reach.

TaskSurfaceWhy
Schema design, iterationCLI or IDE extension, interactiveYou answer design questions as they come up
Migration and query codeCLI with codex --worktreeThe branch and generated SQL stay out of your main checkout. The local database is shared, so give each worktree its own database (createdb app_<branch> and point DATABASE_URL at it)
Proof on production-sized dataCodex cloud task (codex cloud exec)A seeded database runs in a container, and a best-of-N run (--attempts N) costs no local time
Real query plansCLI plus a read-only PostgreSQL MCP serverCodex reads EXPLAIN output from staging instead of guessing
Weekly regression auditDesktop app automation, or codex exec on a scheduleRuns without you and reports only what changed

The CLI flags on this page are checked against codex 0.157.1 and drizzle-kit 0.31.11. The Codex worktrees guide covers the worktree mechanics, and the cross-tool database guide sets Codex beside Claude Code and Cursor.

Codex reads AGENTS.md before it does any work, so database rules belong there rather than in every prompt. Add this block to the repository root:

## Database rules
- Stack: PostgreSQL 16, Drizzle ORM. Schema in src/lib/db/schema.ts, migrations in drizzle/.
- Generate migrations with `npx drizzle-kit generate --name <what_changed>`. Never hand-edit
a migration that exists on main, and never edit drizzle/meta/ by hand.
- Only connect to DATABASE_URL from the environment. Never connect to a host containing
"prod" and never run a migration against anything but a local or test database.
- Indexes on existing tables use CREATE INDEX CONCURRENTLY IF NOT EXISTS in
scripts/concurrent/<index_name>.sql, never in drizzle/: drizzle-kit migrate runs every
pending migration in one transaction, so CONCURRENTLY cannot go through it. Apply the file
with `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f <file>` after `npm run db:migrate`,
then check pg_index.indisvalid for the new index.
- Schema changes on existing tables follow expand/contract: add, backfill, switch reads,
then drop in a later release. Never rename or drop a column in the same release that stops using it.
- If `pg_isready` fails, start PostgreSQL with `service postgresql start`.
- Before you finish: `npm run db:migrate`, then any new scripts/concurrent/ file with psql,
`npm run db:lint`, `npm test -- tests/db`.

npm run db:lint wraps the migration linter shown under verification. The AGENTS.md optimization guide covers how to keep the file short enough that rules like these are followed.

Design the schema before any migration exists

Section titled “Design the schema before any migration exists”

Start in the CLI or IDE extension. Schema mistakes are the most expensive part of this work to undo, so the first prompt forbids generating anything that could be applied.

Push back on the proposal in the same thread: ask what happens to a summary row when an order is refunded a month later, and whether (user_id, day) needs to be unique. Answers to questions like these become the acceptance criteria for the query tests later.

Generate a migration that is safe on a large table

Section titled “Generate a migration that is safe on a large table”

Start a worktree session so the migration lands on its own branch:

Terminal window
# Terminal, repo root
codex --worktree

Three facts about drizzle-kit shape this prompt. It generates forward-only migrations: the 0.31.11 command list (generate, migrate, push, check, up, drop, and others) has no down or rollback command, and drop deletes a migration file rather than reverting a database. A generated CREATE INDEX is a plain, blocking one. And drizzle-kit migrate runs every pending migration in one transaction. So the rollback plan and the concurrent indexes are yours to ask for, and the concurrent indexes cannot live in drizzle/.

The concurrent index sits outside drizzle/ because two rules meet here. PostgreSQL refuses CREATE INDEX CONCURRENTLY inside a transaction block, and drizzle-kit migrate hands all pending migrations to the drizzle-orm PostgreSQL migrator, which applies them in a single transaction (checked in drizzle-orm 0.45.3 on 2026-09-26). A custom Drizzle migration (drizzle-kit generate --custom) runs inside that same transaction, so the index needs its own non-transactional psql step in the deploy, after db:migrate. A failed concurrent build leaves an INVALID index behind, and IF NOT EXISTS then skips it silently; that is why step 4 checks indisvalid rather than trusting a clean exit. Database migration patterns covers expand/contract and backfills in depth.

A query that type-checks and returns rows proves little. The prompt below asks for two kinds of evidence: results checked against an independent SQL oracle, and plans checked for the index each query should use.

The differential test is the one that catches wrong answers: the oracle reads raw orders and knows nothing about the summary tables, so a double-counted refund or a time-zone shift shows up as a mismatch. Plan tests on a small fixture can be misleading, because the planner prefers sequential scans on tiny tables; that is why the prompt puts them in a separate file that runs only with PLAN_TESTS=1, so the local npm test -- tests/db gate skips them and the cloud proof run below turns them on against the production-sized dataset.

Prove the change on production-sized data in a Codex cloud task

Section titled “Prove the change on production-sized data in a Codex cloud task”

Your laptop database has a few thousand rows. The questions that matter (how long the concurrent index takes, whether the planner picks it at 50 million rows) need a bigger dataset. A Codex cloud environment gives each task its own container, so each task can own a disposable PostgreSQL.

Configure the environment’s setup script in the Codex web settings under Environments, and name the environment db-proof: codex cloud exec --env accepts the environment’s name or its ID (checked in codex 0.157.1), and the proof command below uses the name.

#!/usr/bin/env bash
# Codex cloud environment setup script
set -euo pipefail
apt-get update && apt-get install -y --no-install-recommends postgresql postgresql-contrib
npm ci
service postgresql start
su postgres -c "createuser --superuser root" # peer auth for the container's root user, no password
createdb app_test
# Prove the database is reachable. Migrations run in the task, so it can time them
pg_isready

Set DATABASE_URL to postgresql:///app_test?host=/var/run/postgresql as an environment variable in the same settings page, not as a secret, so the agent phase can read it however secrets are scoped (it holds no password anyway). Do not count on the server started by the setup script still running when the agent starts; the AGENTS.md rule above tells the agent how to start it.

Push your branch, then submit the proof task:

Find the task ID with codex cloud list --env db-proof, read the task with codex cloud status TASK_ID, then run codex cloud diff TASK_ID if it changed anything. The report is the artifact you attach to the pull request. A 50-million-row seed takes setup time on every run, so keep the row count to what reveals the plan you care about; the cloud environments guide covers caching and maintenance scripts that keep repeated runs fast.

Let Codex read real query plans through MCP

Section titled “Let Codex read real query plans through MCP”

Seeded data answers “is this safe”. A staging database with real traffic answers “which queries are slow today”. Postgres MCP Pro (PyPI postgres-mcp; last PyPI release 0.3.0, 2025-05-16, checked 2026-09-26) exposes tools such as explain_query, get_top_queries, and analyze_query_indexes. Run it in restricted mode, which limits it to read-only transactions with an execution-time limit, and connect it as a read-only role:

# ~/.codex/config.toml. env_vars forwards DATABASE_URI from your shell; nothing secret is stored here
[mcp_servers.postgres]
command = "uvx"
args = ["postgres-mcp", "--access-mode=restricted"]
env_vars = ["DATABASE_URI"]
enabled_tools = ["list_schemas", "get_object_details", "explain_query", "get_top_queries", "analyze_query_indexes"]

Export DATABASE_URI with the staging read-only connection string before you start codex, and confirm the entry with codex mcp get postgres. The older @modelcontextprotocol/server-postgres package is deprecated on npm; do not copy snippets that use it. The database MCP guide has the SQL for the read-only role and the extensions the index tools need (pg_stat_statements, hypopg). The guide also covers alternatives such as Supabase and DBHub. If a package with no release since May 2025 is a problem for your team, DBHub is still actively released. Codex MCP setup covers approval modes and keeping secrets out of config.toml.

Once the dashboard ships, regressions arrive through ordinary feature work. Schedule the audit as a thread automation in the Codex desktop app, so each run remembers the previous one, with a read-only sandbox and the MCP server above. The Codex automations guide covers setup, sandboxing, and running the same prompt with codex exec on an always-on host.

How do you verify Codex database work without reading every line?

Section titled “How do you verify Codex database work without reading every line?”

Database changes are one of the few change classes where a person still reads the code itself, so the aim is to shrink what that person reads to the migration SQL and a page of evidence. The evidence-over-diffs approach explains the split.

  1. Lint the migration. Squawk (squawk-cli on npm) flags blocking index builds, unsafe column changes, and other lock hazards in PostgreSQL migrations. Wire it up as db:lint, for example npx --package squawk-cli squawk drizzle/*.sql scripts/concurrent/*.sql, and run it in CI on every pull request that touches drizzle/ or scripts/concurrent/.

  2. Apply from zero, twice. CI creates an empty database, runs every migration, runs them again (nothing may re-apply), applies the scripts/concurrent/ files with psql, and runs npx drizzle-kit check to catch inconsistent migration metadata.

  3. Run the differential and plan tests. The query tests compare each Drizzle result with an independent SQL aggregate and assert the intended index. A failure here is a wrong answer or a lost index, not a style issue.

  4. Attach the cloud proof report. The pull request carries the timings, the indisvalid check, and the plans from the production-sized run. No report, no merge.

  5. A named human reads the SQL and signs off. The developer who ran the task reads every .sql file and the rollback note, which are short next to the generated application code. For changes to tables above your size threshold, a second reviewer (the tech lead or a DBA) signs off on the lock analysis. Codex’s @codex review on the pull request is a useful extra pass, not the sign-off.

The application code around the queries (types, service wiring) goes through the same gates as any other Codex change: type check, lint, tests, and review, as described in Codex testing.

What breaks when Codex does database work?

Section titled “What breaks when Codex does database work?”
SymptomLikely causeRecovery
Deploy fails with “CREATE INDEX CONCURRENTLY cannot run inside a transaction block”drizzle-kit migrate applies all pending migrations in one transactionRemove the CONCURRENTLY statement from drizzle/, run it with psql outside the migrator as its own deploy step, drop any INVALID index left behind with DROP INDEX CONCURRENTLY, and retry
drizzle-kit check fails or a migration re-appliesCodex edited an applied migration or drizzle/meta/ by handRevert those files to main, change schema.ts only, and regenerate; keep the AGENTS.md rule that forbids it
Plan tests pass locally, the dashboard is slow in productionThe seed is small or uniformly distributed, so the planner chose differentlySeed with production-like skew, run ANALYZE, and keep plan assertions in the cloud proof run
Codex “fixes” every slow query with a new indexThe prompt rewarded speed onlyRequire each index to cite the query it serves and its plan before and after, and ask which existing index it makes redundant
Dashboard totals are off for users outside UTCDay boundaries computed in UTCPass the user’s time zone into every query, and keep the 23:30 UTC order in the differential fixture
Two worktree sessions fail with “relation already exists” or see each other’s test rowsEvery worktree points at the same local DATABASE_URL, so parallel migrations and fixtures collideCreate one database per worktree (createdb app_<branch>) and set DATABASE_URL in that worktree’s environment
The cloud task reports “connection refused”PostgreSQL started in setup is not running in the agent phaseKeep the pg_isready and service postgresql start rule in AGENTS.md, and check the environment variable holds the socket URL
The MCP server fails to start or shows no toolsuvx missing, DATABASE_URI not exported, or a misspelled name in enabled_toolsRun uvx postgres-mcp --help directly, export the variable before starting codex, and read the real tool names with /mcp verbose