Troubleshooting guide

Database migration not applied in production: causes and fix

The deploy succeeds. Then endpoints start returning 500, a feature stays broken for days, or records go missing after a data migration. Often the migration history says everything was applied. Six verified builder reports in our data describe this, on managed Postgres, Supabase and ORM-based stacks.

10 min read Updated 27 Sep 2026How we collect cases

What you'll see

  • API routes return 500 straight after a deploy, and the logs show errors such as column "plan_id" does not exist or relation "invoices" does not exist.
  • prisma migrate deploy or supabase db push reports nothing to apply, but the production schema is missing the change.
  • A feature stays broken for days after a deploy that reported success.
  • After a data migration, some records are missing or can no longer be edited. Usually these are the rows without a parent row, such as a tenant or account.
  • A migration nobody tested is already live, because development and production point at the same database.
  • prisma migrate deploy refuses to run any further migrations because an earlier one is recorded as failed.

Why it happens

Cause 1

The pipeline never runs migrations

Build and deploy are automated, but applying schema changes is not, so it depends on someone running a command by hand. A related failure: the migration CLI is a dev dependency and the host prunes dev dependencies, so the step cannot run at all. Prisma's deployment docs cover this case and suggest moving prisma to production dependencies on those platforms.

Cause 2

Migrations ran against the wrong database

DATABASE_URL in the pipeline, or on the laptop that ran the command, points at development or a preview branch instead of production. On Vercel, environment variables are scoped to Production, Preview and Development, and changes apply only to new deployments. A corrected value has no effect until you redeploy.

Cause 3

The history table says applied, but the SQL never ran

prisma migrate resolve --applied adds a migration to _prisma_migrations without running its SQL. supabase migration repair --status applied inserts a record into supabase_migrations.schema_migrations the same way. Once that row exists, later deploys skip the migration. prisma migrate deploy does not check for drift, so nothing reports the gap.

Cause 4

Development and production share one database

With one database, a migration first runs against production data. There is no earlier stage where it can fail safely, so a migration that breaks a feature is only found after it is live.

Cause 5

A data migration assumes every row has a relationship

An INSERT ... SELECT with an inner join, or an UPDATE ... FROM, only touches rows that match the join. Rows with a NULL or dangling foreign key, such as events with no tenant, are skipped without an error. The migration reports success and the skipped rows stay in their old state.

How to fix it

  1. 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
  2. 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.

  3. 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
  4. 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';
  5. 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 $$;
  6. 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.

  7. 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.

Check it's fixed

  • prisma migrate status exits 0, or supabase migration list shows the same migrations locally and on the remote.
  • prisma migrate diff or supabase db diff --linked against production reports no schema changes.
  • The information_schema.columns query returns the new column in production, and the endpoints that returned 500 now respond normally.
  • A deliberately broken migration on a staging branch fails the deploy, and the previous version keeps serving.

Fix it with Gemmein

On Gemmein, schema changes reach production only through promotion from development. You run the promotion with npx gemmein go-live or from the dashboard's Go-live page, and it asks what existing records should show for each new field. Development and production are separate environments, and the key prefix shows which one your code is using.

  1. Promote the new collection or field

    Build the change in development first. New collections, and new fields for a live collection whose shape sealed at go-live, reach production only by promotion.

    npx gemmein go-live
  2. Deploy the frontend after the promotion

    Promote first, then ship the code that depends on the new collection or field. A frontend shipped before its promotion reads blanks or refusals from live. The Problems page shows every refused write with the exact refusal your app received, such as 400 invalid_shape.

  3. Decide what existing records show

    For each added field, the promote run asks what existing records should show, because they don't have the field yet. You can leave it blank or give a default, and records that never wrote the field are served that default. Only text, number and yes/no fields can carry a default.

  4. Keep development and production on their own keys

    The key prefix names the rail: pk_test_ and sk_dev_ use the development environment, and pk_live_ uses production. npx gemmein sync copies structure to development but never records or people.

Questions

Why does migrate deploy say there is nothing to apply when the column is missing?

Deploy tools decide what to run from the history table, not from the schema. If a row exists for the migration, the migration is skipped. prisma migrate deploy also does not check for drift. Compare the live schema with prisma migrate diff or supabase db diff --linked.


Should migrations run before or after the new code goes live?

Before, so the new code never runs against an old schema. Write migrations the old code can also run against: add a column before any code uses it, and drop columns in a later release. The version still serving during the rollout then keeps working.


Is it fine to share one database between development and production?

No. Every untested migration runs against production data, and test data ends up next to real users' records. Give each environment its own database and its own credentials.


Can I let my AI coding tool run migrations on production?

Let it write migrations, but have a person review them and apply them through the pipeline. Keep the production database URL out of the agent's environment so it cannot run one directly.


Sources

← Back to the full report