Run migrations as a deploy step that can fail the deploy
On Render, the pre-deploy command runs after the build and before the new version goes live. If it fails, the deploy fails and the previous deploy keeps serving. On Railway, a failed pre-deploy command is not retried and the deployment does not go ahead. On hosts without a pre-deploy step, run the migration in a CI job that the production deploy depends on.
# Render or Railway pre-deploy command, or a CI job that gates the deploy
npx prisma migrate deploy
Give production its own database and credentials
Use separate databases for development, preview and production, each with its own DATABASE_URL scoped to that environment in your host's settings. Don't keep the production URL in a local .env, where a local command or a coding agent can use it by accident. Prisma's docs advise against storing the production database URL locally.
Compare the live schema, not the history table
prisma migrate status exits with code 1 when migrations are unapplied or failed, or when the history has diverged. prisma migrate diff compares the live database with your schema file. On Supabase, supabase migration list shows local and remote history side by side, and supabase db diff --linked diffs your migration files against the linked project.
# Prisma 7 (reads the URL from prisma.config.ts)
npx prisma migrate status
npx prisma migrate diff --from-config-datasource --to-schema=prisma/schema.prisma --script
# Supabase
supabase migration list
supabase db diff --linked
Confirm the change exists in production
Query the catalog for the column or table the new code depends on. If the query returns no row, the migration did not run on this database, whatever the history table says.
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'events'
AND column_name = 'tenant_id';
Make data migrations check for skipped rows
Before a data migration, count the rows the join will not match. If the count is wrong, raise an exception so the transaction rolls back and the deploy step fails. After the migration, check the same way that no row was left in its old state.
DO $$
DECLARE orphans int;
BEGIN
SELECT count(*) INTO orphans
FROM events e
LEFT JOIN tenants t ON t.id = e.tenant_id
WHERE t.id IS NULL;
IF orphans > 0 THEN
RAISE EXCEPTION '% events have no tenant', orphans;
END IF;
END $$;
Only mark a migration applied after checking the schema
prisma migrate resolve --applied and supabase migration repair --status applied only write to the history table. Run them only after you have confirmed the change exists in the schema. If a migration is recorded but never ran, supabase migration repair --status reverted deletes the record so the next supabase db push applies it. With Prisma, create a new migration that contains the missing change.
Review any production migration an agent proposes
Read the SQL an AI coding tool generates before it reaches production, and run it against a staging copy first. Keep production credentials out of the environment the agent runs in, so it cannot apply a migration directly.