Skip to main content

Migrations

Schema-first. Generate SQL artifacts, review, commit, apply.

The cardinal rule​

Never run drizzle-kit push against any non-local DB.

push compares the schema to the target DB and applies the diff with no SQL artifact. It's the canonical horror story for prod data loss. Always go through generate → review → migrate.

Workflow​

# 1. Edit schema
$EDITOR src/db/schema/business.ts

# 2. Generate SQL
pnpm db:generate --name add_my_column
# → drizzle/0017_add_my_column.sql

# 3. REVIEW THE GENERATED FILE
$EDITOR drizzle/0017_add_my_column.sql

# 4. Commit
git add src/db/schema/ drizzle/
git commit -m "Add my_column to agents"

# 5. Apply
pnpm db:migrate # dev (uses ts-node)
pnpm db:migrate:prod # prod (compiled, in Docker)

Auto-generated auth schema​

Don't hand-edit src/db/schema/auth.ts. After upgrading better-auth or a plugin:

pnpm db:auth-schema       # runs @better-auth/cli generate
pnpm db:generate --name auth_upgrade
# review + migrate

The auth.ts file has a frontmatter comment saying this. If a PR touches it by hand, reject.

What generate emits​

drizzle-kit generate produces:

  • drizzle/NNNN_<slug>.sql — the migration SQL.
  • drizzle/meta/_journal.json — registry of applied migrations.
  • drizzle/meta/NNNN_snapshot.json — schema snapshot for diffing.

Each statement is separated by --> statement-breakpoint so the migrator can run them one at a time.

Rare patterns​

Drizzle handles most cases automatically. For the rare cases (partial indexes, CHECK constraints, GENERATED columns), express them in TypeScript with sql\...``:

.where(sql`status IN ('active', 'trialing')`)

These come through generate correctly.

Standalone migrator​

src/db/migrate.ts is a standalone Node script:

  • Loads .env itself.
  • Resolves drizzle/ via process.cwd().
  • Works under both ts-node (dev) and node dist/src/db/migrate.js (prod).

docker-entrypoint.sh runs it on every container start. Migrations are tracked in __drizzle_migrations; re-running an applied migration is a no-op.

Rollback​

There is no automatic down migration. If you need to undo:

  1. Write a new migration that reverses the change.
  2. Generate and apply it like any forward migration.

This is intentional. Drizzle's stance is "down migrations are dangerous in production"; just write a fix-forward migration.

Common patterns​

Add a non-null column with default​

isArchived: boolean("is_archived").notNull().default(false)

Generates ALTER TABLE ... ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT false; — safe on existing rows.

Add a non-null column with no default​

Two-step migration:

  1. Add as nullable.
  2. Backfill in a follow-up migration.
  3. ALTER to set NOT NULL once backfill is verified.

Rename a column​

Drizzle treats column renames as DROP + ADD by default. Use .rename or write a manual ALTER TABLE ... RENAME COLUMN ... migration to preserve data.

Add a unique constraint​

uniqueIndex("agent_pricing_tiers_unique_active")
.on(table.agentId, table.tierName)

Verify before deploy​

pnpm db:migrate:dry         # if you've added a dry-run flag
psql -1 -f drizzle/0017_add_my_column.sql $DATABASE_URL # one-shot transaction

Drizzle Studio​

pnpm db:studio              # opens at https://local.drizzle.studio

Useful for inspecting rows and verifying migration effects without dropping into psql.