Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Drizzle ORM migrations -- generate vs push, when to use each in production

May 28, 2026
drizzle-ormneon-dbmigrationsnextjsdatabase

Drizzle ships two schema sync commands: db:generate and db:push. They look similar but they are not interchangeable. Using the wrong one in production is how you silently drop columns or lose data.

This post covers what each command actually does, when to use each, and how to wire safe migrations into a Next.js App Router project deployed on Neon DB.

Already familiar with schema definition and query patterns? See Drizzle ORM with Neon DB -- migrations, relations, and query patterns for the broader setup.

What each command does

db:push reads your TypeScript schema and syncs it to the database directly -- no SQL files, no history. It is convenient locally because it is fast and requires no migration files. But it will silently drop columns that no longer appear in your schema, and it leaves no audit trail.

db:generate reads your schema and produces a SQL migration file in the drizzle/ folder. Nothing is applied to the database yet. You review the SQL, commit it, and apply it separately with db:migrate.

This two-step flow gives you reviewable, version-controlled SQL that runs identically on every environment.

The rule

  • Local development: db:push. Fast, no overhead.
  • Staging and production: db:generate + db:migrate. Never db:push.

Config

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
 
export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

The out directory is where migration files are written. Commit the drizzle/ folder to git -- it is your migration history.

Generating a migration

After any schema change, run:

npm run db:generate

This adds a file like drizzle/0001_add_posts_table.sql. Each file is plain SQL with a metadata header Drizzle uses to track state. Do not edit these files by hand.

drizzle/
  0000_initial.sql
  0001_add_posts_table.sql
  0002_add_tags_column.sql

Applying migrations

npm run db:migrate

Drizzle runs pending files in order and records them in a __drizzle_migrations table. Already-applied files are skipped automatically.

For a deploy script, call the migrator programmatically before starting the app:

// scripts/migrate.ts
import { drizzle } from "drizzle-orm/neon-http";
import { migrate } from "drizzle-orm/neon-http/migrator";
import { neon } from "@neondatabase/serverless";
 
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
 
await migrate(db, { migrationsFolder: "./drizzle" });
console.log("Migrations applied");
process.exit(0);

Wire it into your start command:

npx tsx scripts/migrate.ts && npm run start

On Vercel, add this as a build command or pre-deploy step. With Neon DB, the connection is serverless so migrate.ts runs safely inside a Vercel Function context.

Using Neon DB branching to test before production

Neon DB branching is the safest way to validate a migration before it touches production. Each pull request gets its own database branch -- a full copy of the production schema.

# create a database branch per PR via the Neon CLI or MCP server
neon branches create --project-id <id> --name preview/my-feature

Point the preview deployment's DATABASE_URL at that branch and run db:migrate as part of the preview build. If the migration fails or breaks queries, the branch is discarded. Production is untouched.

This is the workflow this SaaS boilerplate is built around: git feature branch + matching Neon branch + automatic db:migrate on deploy. Merge when the preview is green.

What breaks if you skip generation

If you edit a schema file and forget to run db:generate, your full-stack TypeScript types and the actual database schema diverge. Drizzle does not catch this at build time -- it fails at query time when it references a column that does not exist.

Claude Code prevents this with the drizzle-migrate skill: any session that edits a schema file gets a reminder to run db:generate before committing. In this boilerplate, that rule is encoded in CLAUDE.md so it holds across every session.

Checklist for every schema change

[ ] Edit schema file in db/ or modules/*/
[ ] npm run db:generate
[ ] Review the generated SQL in drizzle/
[ ] git add drizzle/ && git commit
[ ] npm run db:migrate (staging first, then production)

Never run db:push on any environment with real data. Always review destructive migrations (DROP COLUMN, DROP TABLE) before applying.

Takeaway

db:push is a local shortcut. For every other environment, use db:generate to produce a reviewable SQL file and db:migrate to apply it. Commit the drizzle/ folder to git, wire migrate.ts into your deploy, and use Neon DB branching to test schema changes against a real copy of your data before they hit production.