Committed migrations were never applied to the live database
why does my live database not match my migration files Yes - schema drift happens because deploys copy code but nothing runs pending migration files against the live database, and because someone changed a policy or grant by hand on production without writing it down as a migration. Fix it by diffing the live schema against the migrations directory, adopting a runner with a tracked migrations table, and running migrations as a required deploy step.
Seen in 14 of 450 scanned projects (3%). Based on 450 Deep Scan runs against self-hosted Postgres and Supabase projects.
How to tell you have it
- A migration file sits in the repo for weeks but a query against the live table shows the old column set
- An RLS policy or grant exists in production psql output but has no matching file in the migrations directory
- Deploys succeed and the app boots fine even though a migration never ran
- Two engineers get different results from the same query because their local databases were seeded from different migration states
- A scheduled job or trigger runs in production with no corresponding SQL checked into the repo
Why it matters
When a deploy script only pulls code and restarts a process, any SQL file sitting in a migrations folder is inert. It looks like part of the codebase, so reviewers assume it already ran. Weeks later a feature depends on a column that was never added, and the failure shows up as a runtime error in production instead of a failed deploy, which makes it far harder to trace back to the missing step.
The reverse problem is just as common. Someone opens psql against the live database to fix an urgent RLS policy or revoke a grant during an incident, then forgets to turn that fix into a migration file. The live schema is now ahead of the repo. The next person who runs the migrations from scratch on a fresh environment gets a database that behaves differently than production, and nobody notices until a bug only reproduces in one environment.
Scheduled jobs and cron-style database tasks are especially prone to this because they are often created through a dashboard or a one-off psql session rather than a file. If the job is dropped or the extension is disabled during a schema rebuild, there is no artifact to reapply it from, and the person who set it up may no longer be around to remember it existed.
Once drift accumulates in both directions, the migrations directory stops being trustworthy. Engineers start avoiding it, testing changes by hand against production instead of writing a migration, which compounds the problem. Rolling back a bad release becomes guesswork because nobody can say with confidence which of the committed migrations actually reflect what is live.
How Heygents detects it
Deep Scan reads the migrations directory with `ls -la` and `git log` against it to see which files are committed and when, then connects with `psql` using the project's own DATABASE_URL from its `.env` to list actual tables, RLS policies, and role grants, comparing that live schema against what the migration files would produce. It also checks `git status --porcelain` for migration files that were run locally but never committed.
How to fix it
- Diff the live schema against the migrations directory Before adopting any tooling, find out how far the drift actually goes. Dump the live schema with pg_dump in schema-only mode and compare it against what running every migration file in order would produce in a scratch database.
- Adopt a migration runner with a tracked migrations table Stop treating the migrations folder as documentation. Use a runner that records which files have been applied in a table on the database itself, so state lives next to the data instead of in someone's memory. node-pg-migrate, Prisma Migrate, and Supabase CLI migrations all work this way.
- Backfill hand-applied changes into migration files For every policy, grant, or scheduled job that exists live but has no file, write a migration that recreates it, then mark it applied without rerunning it against production since it is already there. This closes the gap without touching a live table twice.
- Run migrations as a required deploy step, not an afterthought A migration runner only prevents drift if it is actually invoked on every deploy, and the deploy must fail loudly if it does not. Wire it into the same script that restarts the app, before the restart, not after.
- Add a drift check to your weekly review Even with a runner in place, someone will eventually patch production by hand during an incident. Schedule a recurring, read-only comparison of live schema against the migrations table so drift gets caught within a week instead of months later.
Diff the live schema against the migrations directory
pg_dump --schema-only --no-owner --no-privileges "$DATABASE_URL" > live_schema.sql
# in a throwaway local/staging database, apply migrations in order
for f in migrations/*.sql; do psql "$SCRATCH_DATABASE_URL" -f "$f"; done
pg_dump --schema-only --no-owner --no-privileges "$SCRATCH_DATABASE_URL" > from_migrations.sql
diff live_schema.sql from_migrations.sql
Adopt a migration runner with a tracked migrations table
npm install --save-dev node-pg-migrate
# baseline the current live state as already-applied so it does not
# try to rerun history that is already reflected on disk
npx node-pg-migrate up --dry-run
npx node-pg-migrate up
Backfill hand-applied changes into migration files
-- migrations/2026073101_backfill_orders_rls.sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_owner_select ON orders
FOR SELECT
USING (auth.uid() = user_id);
REVOKE ALL ON orders FROM anon;
GRANT SELECT, INSERT, UPDATE ON orders TO authenticated;
Run migrations as a required deploy step, not an afterthought
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/<app>
git pull origin main
npm ci
# fail the deploy if migrations do not apply cleanly
npx node-pg-migrate up
pm2 restart <app>
Add a drift check to your weekly review
psql "$DATABASE_URL" -c "SELECT name, run_on FROM pgmigrations ORDER BY run_on DESC LIMIT 10;"
ls -la migrations/ | tail -10
Find this in your own projects, automatically
Heygents runs a read-only Deep Scan across every project you own, finds issues like this one, and hands you a ready-to-run fix an AI agent can execute and verify. A solo developer gets the audit, the backlog and the fix loop in one place.
Open Heygents →Frequently asked questions
How do migrations end up unapplied in the first place if the file is right there in the repo?
Most deploy scripts only pull code and restart the process. Unless a migration command is explicitly wired into that script, a SQL file in the repo is just text. It gets reviewed and merged like any other change, which creates a false sense that it already ran, when in fact nothing in the deploy pipeline ever executed it against the live database.
Is it safe to just run every migration file against production to catch up?
Not without checking first. If the live database already has some of those changes applied by hand, rerunning the same CREATE POLICY or ALTER TABLE statement can fail or, worse, silently produce a different result than intended. Diff the live schema against what the migrations would produce before running anything, and make each migration idempotent where possible.
What is the fastest way to start tracking migration state without a big rewrite?
Add a lightweight runner like node-pg-migrate or the Supabase CLI, create its bookkeeping table, and mark your existing migration files as already applied since they reflect what should be live. From that point forward every new change goes through the runner, and the table gives you an audit trail without touching existing data.
How do I stop engineers from patching production by hand during incidents?
You will not stop it entirely during a real outage, and that is fine. What matters is a habit of writing the fix as a migration file immediately after, even if it is applied to production after the fact rather than before. Treat any hand-applied change as incomplete work until it exists as a file in the repo.
Can scheduled jobs and cron-style database tasks be captured in migrations too?
Yes, most Postgres schedulers such as pg_cron store jobs as rows in a table, which means the CREATE or SELECT cron.schedule call that created them can be written into a migration file just like any DDL. Doing this means rebuilding a database from migrations recreates the job instead of leaving it silently missing.