Drizzle ORM Migrations: A Practical drizzle-kit Guide
Learn the full Drizzle ORM migration workflow: push vs migrate, drizzle-kit setup, Turso/libSQL config, team conflicts, and production best practices.
On this page
If you've added Drizzle to your Next.js project and gotten confused by push vs migrate, or you're not sure how to handle migrations in a team without conflicts, this guide covers the actual workflow. Not just the happy path.
This is the workflow DevEncyclopedia used when migrating its own production database from MongoDB to Turso. The Turso-specific setup reflects what we actually deployed.
push vs migrate: The Most Important Distinction
Drizzle is split into two packages: drizzle-orm (the runtime) and drizzle-kit (the CLI). Your schema is TypeScript. drizzle-kit reads it and does one of two things:
| drizzle-kit push | drizzle-kit generate + migrate | |
|---|---|---|
| Creates SQL files | No | Yes — committed to Git |
| Tracks history | No | Yes — __drizzle_migrations table |
| Safe for production | Never | Yes |
| Speed | Instant | Fast |
| Best for | Local dev iteration | All team and production changes |
# Development — fast, no files
npx drizzle-kit push
# Production — generate first, review SQL, then apply
npx drizzle-kit generate
npx drizzle-kit migrateSetting Up drizzle-kit
- 1
Install and configure drizzle-kit
Create
drizzle.config.tsin your project root. This example uses PostgreSQL. Turso config is in Step 3:typescript — drizzle.config.tsimport { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/db/schema.ts', out: './migrations', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL!, }, }); - 2
Define your schema and generate the first migration
typescript — src/db/schema.tsimport { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), createdAt: timestamp('created_at').defaultNow(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), authorId: integer('author_id').references(() => users.id), publishedAt: timestamp('published_at'), });bashnpm run db:generate # Creates ./migrations/0001_initial.sql - 3
Turso/libSQL-specific setup
If you're using Turso, the dialect, credentials, and column types all change from PostgreSQL:
typescript — drizzle.config.ts — Tursoimport { config } from 'dotenv'; import { defineConfig } from 'drizzle-kit'; config({ path: '.env.local' }); // drizzle-kit does not auto-load .env.local export default defineConfig({ schema: './src/db/schema.ts', out: './migrations', dialect: 'turso', dbCredentials: { url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }, });- Install the Turso driver:
npm install @libsql/client - Use
sqliteTablefromdrizzle-orm/sqlite-coreinstead ofpgTable - Use
integer('id').primaryKey({ autoIncrement: true })instead ofserialfor auto-increment IDs
- Install the Turso driver:
- 4
Run migrations in production
Run migrations as part of your deployment process, not on application startup:
bash — Vercel — add to Build Commandnpm run db:migrate && next buildbash — Cloudflare — run before wrangler deploynpm run db:migrate npx wrangler deploy
Troubleshooting
Edge cases you'll hit when working in a team:
- 1
Handling team migration conflicts
When two branches both add schema changes and merge, you can end up with two files both numbered
0002_something.sql. Fix by regenerating from the resolved schema:bash# 1. Resolve merge conflicts in schema.ts # 2. Delete the conflicting migration files # 3. Regenerate from the resolved schema npm run db:generate # Creates one clean migration covering all merged changes - 2
Reverting a migration
Drizzle does not generate automatic rollback files. To reverse a migration, write the reverse SQL by hand as a new forward migration:
- Additive changes (adding a table, adding a nullable column) are safe and rarely need reverting
- Destructive changes (dropping a column, changing a type) require a hand-written reverse migration and should always be tested on staging first
- Always export a database backup before any destructive production migration
drizzle-kit Commands Reference
| Command | What it does | When to use |
|---|---|---|
| drizzle-kit generate | Creates SQL migration files from schema diff | Every time you change schema.ts |
| drizzle-kit migrate | Applies pending migration files to the database | Before every deployment |
| drizzle-kit push | Syncs schema directly without files | Development only — never production |
| drizzle-kit studio | Opens Drizzle Studio GUI in your browser | Browsing/editing data locally |
| drizzle-kit introspect | Generates schema.ts from an existing database | Migrating from Prisma or another ORM |
Frequently Asked Questions
What is the difference between drizzle-kit push and drizzle-kit migrate?
| push | generate + migrate | |
|---|---|---|
| SQL files created? | No | Yes |
| History tracked? | No | Yes |
| Safe for production? | Never | Yes |
Use push in development for speed. Use generate + migrate for every production change.
Why does drizzle-kit push fail with 'url: undefined' on Turso?
drizzle-kit does not automatically load .env.local. Add this to the top of drizzle.config.ts:
import { config } from 'dotenv';
config({ path: '.env.local' });Also install dotenv as a dev dependency: npm install -D dotenv.
How do I handle migration conflicts when multiple developers change the schema?
- Resolve the conflict in
schema.tsfirst — get both sets of changes into one clean schema - Delete the conflicting migration files (both
0002_branch_a.sqland0002_branch_b.sql) - Run
drizzle-kit generateto produce one clean migration covering all merged changes - Commit migration files to Git and review them in pull requests the same way you review code
When should I run drizzle-kit generate?
Every time you change schema.ts. Commit the generated migration file alongside the schema change. In CI you can add a check that fails the build when a schema change exists without a corresponding migration file, preventing deployments with unapplied changes.
I'm migrating from Prisma. How is Drizzle's migration workflow different?
| Prisma | Drizzle | |
|---|---|---|
| Generate + apply in one command? | Yes (migrate dev) | No — separate generate and migrate |
| Shadow database needed? | Yes | No |
| Migration seeding? | Yes | No |
| Schema language | Prisma Schema Language (.prisma) | TypeScript |
Drizzle gives you more visibility and control. You are responsible for running generate before each deployment — it doesn't happen automatically.
The Drizzle migration workflow is straightforward once the push vs migrate distinction clicks: push to move fast in development, generate + migrate to make changes reviewable and auditable before touching production.
Commit your migration files, export a backup before destructive changes, and wire db:migrate into your deployment pipeline. That's the entire production discipline.
Related Articles
TypeScript 7 (Project Corsa): What Next.js Devs Need to Know
TypeScript 7 rewrites the compiler in Go for 10x faster builds. Here's what it means for your Next.js project and what to do right now.
How to Use Environment Variables in Next.js (Without Leaking Them to the Browser)
Learn how to use .env files in Next.js correctly. Understand NEXT_PUBLIC_, avoid common mistakes, and set variables in Vercel and Cloudflare.
Husky + Prettier + lint-staged Setup for Next.js
Set up Husky v9, Prettier, and lint-staged in your Next.js project. Step-by-step guide covering pre-commit hooks with the correct 2026 config.