ORM Migration Failures in Production: The Complete Guide
Prisma, Drizzle, and TypeORM each have their own migration system and their own ways to fail in production. Here is what goes wrong with each one and how to recover.
Every major JavaScript ORM ships with a migration system. In development, these systems work well enough that you rarely think about them. In production, they are one of the most common sources of deployment failures and data incidents.
The failure modes differ by ORM. Here is what goes wrong with Prisma, Drizzle, and TypeORM, and how to handle each.
Prisma
Prisma manages migrations through a folder called prisma/migrations. Each migration is a SQL file with a timestamp. Prisma tracks which migrations have been applied in a table called _prisma_migrations in your database.
The production command is prisma migrate deploy. This is not the same as prisma migrate dev. The dev command generates new migration files and is for development only. The deploy command applies pending migrations and is the only Prisma command that should run in production.
The most common failure: running prisma db push instead of migrations. prisma db push applies your current schema directly without creating migration files. It is fine for prototyping. The moment you have a production database with real data, you need proper migrations. If you used db push during development and then try to use migrate deploy in production, Prisma has no migration history to apply and cannot figure out what to do.
Schema drift: If your production database was modified outside of Prisma, for example a column added manually through a GUI or a raw SQL script run by someone, the database state no longer matches what Prisma's migration history expects. migrate deploy detects this and stops. The fix is prisma migrate resolve, which lets you mark a migration as applied (if you applied it manually) or rolled back. This requires knowing exactly what state your database is in.
Failed migration: If a migration fails partway through, the _prisma_migrations table records it as failed. Subsequent runs of migrate deploy stop when they hit the failed entry. You need to fix the underlying issue, then use prisma migrate resolve --rolled-back <migration-name> to mark it as rolled back, then run migrate deploy again.
Recovery checklist for Prisma:
- Check
_prisma_migrationsfor any entries with a failed status. - Use
prisma migrate resolveto get back to a clean state. - If there is schema drift, compare
prisma db pulloutput against your schema file to understand what changed. - Run
prisma migrate deploywith the production database URL explicitly set.
Drizzle
Drizzle is newer and takes a different approach. It generates SQL migration files that you review and apply, giving you more control but also more responsibility.
The migration command depends on your setup. If you are using drizzle-kit, migrations are generated with drizzle-kit generate and applied with drizzle-kit migrate or with a custom script that calls migrate() from drizzle-orm/migrator.
The most common failure: calling the Drizzle migrator in application code without waiting for it to complete before serving traffic. The pattern looks like this:
const db = drizzle(client)
migrate(db, { migrationsFolder: './drizzle' }) // no await
app.listen(3000)
Without awaiting the migration, the app starts serving requests while migrations may still be running. Queries against the new schema fail because the tables or columns do not exist yet.
Missing migrations folder: Drizzle migrations must be present in the deployment. If your .gitignore excludes the migrations folder, or your build step does not copy it to the output directory, the migrator has nothing to apply and either does nothing or throws an error depending on the configuration.
Dialect mismatch: Drizzle has separate migration drivers for PostgreSQL, MySQL, and SQLite. Using the wrong driver does not always fail obviously. It may apply a subset of migrations or silently skip statements that are not valid for the driver it selected.
Recovery checklist for Drizzle:
- Verify the migrations folder is present in your deployment artifact.
- Ensure the migrator is awaited before the app starts serving traffic.
- Verify the correct dialect driver is in use.
- Check that the database URL in production points to the right database.
TypeORM
TypeORM has a migration system built around entity decorators and migration files. The production command is typeorm migration:run, which applies pending migrations from the migrations directory.
The most common failure: synchronize: true in production configuration. This TypeORM option automatically syncs the database schema to match your entities on every startup. It is designed for development. In production, it will alter, drop, and recreate columns without any migration history, without any rollback capability, and without any warning. It is the fastest way to destroy production data.
Every TypeORM production configuration should have synchronize: false. This is not optional.
// Never do this in production
const dataSource = new DataSource({
synchronize: true, // WILL DROP COLUMNS
})
Migration files not loaded: TypeORM needs to know where your migration files are. The migrations option in your DataSource config needs to point to the compiled JavaScript files, not the TypeScript source. After a build, migration files are typically in dist/migrations/**/*.js. Pointing to the wrong path means TypeORM finds no migrations to run.
Circular dependency in entities: TypeORM migrations sometimes fail in production because entity files have circular imports that were not a problem in development (where module loading order was different). The error looks unrelated to migrations but the fix is in the entity definitions.
Recovery checklist for TypeORM:
- Confirm
synchronize: falsein all production DataSource configurations. - Verify the
migrationspath points to compiled JavaScript files. - Run
typeorm migration:showto see which migrations are pending and which are applied. - Run
typeorm migration:runexplicitly against the production database to apply pending migrations.
What all three have in common
Regardless of ORM, the safe pattern for production migrations is:
The migration command runs against the production database before the new version of the app starts serving traffic. If the migration fails, the deployment stops and the old version stays up. If the migration succeeds, the new app version starts.
This ordering matters. New code that references a column that does not exist yet will fail on every request until the migration runs. Migrations that fail partway through can leave the database in an inconsistent state. Both of these problems are easier to recover from when the old app version is still running.
Jetpacked detects your ORM from your dependencies and schema files, runs the appropriate migration command during deployment before your app starts, and stops the deployment if the migration fails. The deploy log shows exactly what ran and what the output was.
Deploy your app in minutes
Jetpacked handles Docker, HTTPS, databases, and deployments so you can focus on building.
Launch your app free