What the agent does
The agent checks out the pull request's branch in its own sandbox and finds where migrations live before reading any SQL. In a Next.js app like nextjs/saas-starter, drizzle.config.ts points schema at ./lib/db/schema.ts and out at ./lib/db/migrations with dialect: 'postgresql', and package.json has db:generate (drizzle-kit generate) and db:migrate (drizzle-kit migrate). It lists the new .sql files in that folder and the new entries in lib/db/migrations/meta/_journal.json, which is what drizzle-kit migrate reads to decide what to apply. A migration file with no journal entry, or a journal entry with no file, is the first thing it flags, because one of them never runs.
Next it compares the migration with the schema it claims to produce. It runs drizzle-kit generate on the branch: if the generated output is not empty, the committed SQL and schema.ts disagree, and the comment says which table or column differs. It also reads how the project already writes migrations. The starter has a single migration, 0000_soft_the_anarchist.sql, which wraps each foreign key in a DO $$ BEGIN ... EXCEPTION WHEN duplicate_object THEN null; END $$; block and creates tables with IF NOT EXISTS, and schema.ts defines no secondary indexes, so every foreign key on activity_logs, invitations and team_members is unindexed today.
Then it runs the migrations for real. It starts Postgres 16.4 in the sandbox, the version lib/db/setup.ts writes into its docker-compose.yml, applies the main branch's migrations, and inserts rows into users, teams and activity_logs with plain SQL so the tables are not empty. It does not use pnpm db:seed, because lib/db/seed.ts creates products and prices through the Stripe API. Then it applies the pull request's migration with POSTGRES_URL pointing at that database. While it runs, the agent checks pg_locks for AccessExclusiveLock on existing tables and times each statement, so a lock or a rewrite is observed rather than guessed.
With that output it reviews each statement. ALTER TABLE ... ADD COLUMN ... NOT NULL without a default on a populated table, ALTER COLUMN ... TYPE that rewrites the table, CREATE INDEX without CONCURRENTLY on a large table, a foreign key added without NOT VALID, a DROP COLUMN the app code in lib/db/queries.ts still selects, or a varchar(255) narrowed to varchar(50) all get a comment with the safe sequence instead: add the column nullable, backfill, then set NOT NULL; add the constraint NOT VALID and validate it in a second migration. Drizzle writes no down migrations, so for each destructive statement the comment also says how to undo it by hand.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Review the migrations added in the open pull request. Read drizzle.config.ts and lib/db/migrations/meta/_journal.json, check that each new SQL file has a journal entry, and run drizzle-kit generate to confirm the migrations match lib/db/schema.ts. Start a fresh Postgres in the sandbox, apply the main branch's migrations, insert sample rows with SQL (lib/db/seed.ts calls Stripe, so do not run it), then apply the new ones with drizzle-kit migrate while watching pg_locks. Comment on any statement that rewrites or locks a whole table, drops or narrows a column with data, adds a foreign key without an index, or cannot be rolled back, with the safe sequence to use instead. Do not change the migration yourself.",
"resources": [
{
"url": "https://github.com/your-org/your-app/pull/123"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a review on the pull request with comments on the lines of the new .sql file under lib/db/migrations/. Each comment names the statement, what it does to an existing table (a full rewrite, an AccessExclusiveLock, a dropped column), the evidence from the sandbox run, and the replacement sequence, often split into two migrations so the slow step does not hold a lock. When the migration and schema.ts disagree, the comment quotes the extra SQL drizzle-kit generate produced.
The session's transcript keeps the evidence: the Postgres container start, the main migrations and the sample rows, drizzle-kit migrate output for the new file, and the pg_locks rows seen during each statement. If a migration fails to apply at all, the review leads with the error and the statement that raised it.
Before merging, a reviewer weighs each comment against the real table sizes in production, which the sandbox does not have, and decides whether a lock on a small table is acceptable. A dropped or narrowed column that code still reads is the comment that should block the merge.
Run it on every pull request that touches lib/db/migrations
An automation with a GitHub trigger on pull_request.opened and pull_request.synchronize starts this review; to keep it to schema changes, have your server receive GitHub's pull_request webhook, check whether the changed files include lib/db/migrations/, and only then send this body with the pull request URL in resources. Use the head SHA as the Idempotency-Key so a redelivery does not start a second review.
When the author pushes a fix, send a follow-up message to the same session with POST /o/{org}/sessions/{id}/messages. The parked sandbox wakes with its Postgres data directory and the main migrations already applied, so the second pass only applies the changed file.
The use case this job follows, with its automation and trigger: Review every pull request.
Why run it on Rush?
Most migration reviews read the SQL and guess. Knowing whether a statement takes an AccessExclusiveLock or rewrites the table depends on the Postgres version and the column types, and the only sure way to know is to run it against a database with rows in it. Doing that for every pull request on a laptop means resetting a local database each time and risking the one you develop against. On Rush the agent gets its own sandbox with its own Postgres, built from main for each review, and throws it away afterwards.
The comments come with the run that produced them. A reviewer can read the lock rows and the timings for each statement in the session, rather than trusting an opinion about locks. Reviews for pull requests on different branches run at once, each against its own database, so two schema changes in flight never interfere.
Frequently asked questions
Does it run migrations against my real database?
No. It starts a new Postgres in its own sandbox, applies the main branch's migrations and sample rows, and applies the pull request's migration there. Your POSTGRES_URL is never given to it unless you add it, and the prompt does not ask for it.
How does it know a statement locks the table?
It applies the migration while querying pg_locks from a second connection and reports the lock mode it saw on each table. It also reads the statement against Postgres's documented behavior, such as CREATE INDEX without CONCURRENTLY blocking writes, and says which of the two a comment is based on.
Will it catch drift between schema.ts and the migration?
Yes. It runs drizzle-kit generate on the branch, and any SQL that command produces is drift: the schema says one thing and the committed migration another. The comment quotes the generated statements so the author can regenerate rather than hand-edit.
Does this work with Prisma instead of Drizzle?
The same review works for Prisma migrations under prisma/migrations/. Change the prompt to apply them with prisma migrate deploy and to check drift with prisma migrate diff, and the rest of the review is the same.