Drizzle-Kit vs Atlas vs dbmate Migrations 2026
Drizzle-Kit vs Atlas vs dbmate: DB Schema Migrations 2026
TL;DR
Schema migrations are one of the riskiest operations in any production system — the wrong migration can corrupt data or take down your database. Drizzle-Kit is the migration toolchain for Drizzle ORM — TypeScript-first, generates SQL from your ORM schema definitions, with an interactive UI for reviewing changes. Atlas is schema-as-code from HashiCorp veterans — it diffs your desired schema against your actual database, detects drift, supports HCL or SQL schema files, and integrates with CI/CD for automated migration checks. dbmate is the simple SQL migration runner — language-agnostic, uses plain .sql files with -- migrate:up and -- migrate:down markers, zero magic. For TypeScript/Drizzle ORM projects: Drizzle-Kit. For schema drift detection and CI/CD gates: Atlas. For simple teams that just want SQL files with versioning: dbmate.
Key Takeaways
- Drizzle-Kit generates migrations from TypeScript schema —
drizzle-kit generatecompares schema to previous state, produces SQL - Atlas detects schema drift — compares live DB to desired state; fails CI if they diverge
- dbmate uses plain SQL files — no magic, no framework, any language can use it
- Atlas supports multiple databases — PostgreSQL, MySQL, SQLite, SQL Server, ClickHouse
- Drizzle-Kit introspects existing DBs —
drizzle-kit introspectpulls schema from existing DB into TypeScript - Atlas can visualize schema — generates ERD diagrams from your schema
- dbmate tracks versions in a
schema_migrationstable — same pattern as Rails/Flyway
The Migration Workflow Problem
Database migrations need to be:
1. Reversible (rollback on failure)
2. Versioned (ordered, tracked, non-repeating)
3. Reviewable (humans should see what's changing)
4. Safe in CI/CD (fail fast before reaching production)
5. Team-friendly (no conflicts when multiple devs migrate)
Each tool solves these differently:
Drizzle-Kit: TypeScript schema → auto-generate SQL diffs
Atlas: Desired state → diff against DB → apply safely
dbmate: Manual SQL files → versioned runner
Drizzle-Kit: TypeScript-Native Migrations
Drizzle-Kit is the companion CLI for Drizzle ORM. Define your schema in TypeScript, and Drizzle-Kit generates the migration SQL automatically.
Installation
npm install drizzle-orm drizzle-kit
npm install postgres # or pg, mysql2, better-sqlite3
Schema Definition
// src/db/schema.ts
import { pgTable, uuid, text, integer, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
role: text("role", { enum: ["admin", "user", "moderator"] }).notNull().default("user"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const posts = pgTable("posts", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
slug: text("slug").notNull().unique(),
content: text("content"),
authorId: uuid("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
published: boolean("published").notNull().default(false),
publishedAt: timestamp("published_at", { withTimezone: true }),
viewCount: integer("view_count").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
authorIdx: index("posts_author_idx").on(table.authorId),
publishedIdx: index("posts_published_idx").on(table.published, table.publishedAt),
}));
Drizzle Config
// drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true, // Require explicit confirmation for destructive changes
} satisfies Config;
Generating and Applying Migrations
# Generate migration based on schema changes
npx drizzle-kit generate
# This creates:
# drizzle/0000_initial.sql ← SQL migration file
# drizzle/meta/ ← Schema snapshots for diffing
# Open interactive UI to review migrations
npx drizzle-kit studio
# Apply pending migrations
npx drizzle-kit migrate
// src/db/index.ts — programmatic migration
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!);
export const db = drizzle(sql);
export async function runMigrations() {
console.log("Running migrations...");
await migrate(db, { migrationsFolder: "./drizzle" });
console.log("Migrations complete.");
}
Generated Migration SQL
-- drizzle/0000_initial.sql (auto-generated by drizzle-kit)
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"name" text NOT NULL,
"role" text DEFAULT 'user' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
CREATE TABLE IF NOT EXISTS "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"slug" text NOT NULL,
"content" text,
"author_id" uuid NOT NULL,
"published" boolean DEFAULT false NOT NULL,
"published_at" timestamp with time zone,
"view_count" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "posts_slug_unique" UNIQUE("slug")
);
CREATE INDEX IF NOT EXISTS "posts_author_idx" ON "posts" ("author_id");
CREATE INDEX IF NOT EXISTS "posts_published_idx" ON "posts" ("published","published_at");
ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_users_id_fk"
FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
Adding a Column (Schema Evolution)
// Modify schema.ts — add a bio field to users
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
bio: text("bio"), // ← Added
avatarUrl: text("avatar_url"), // ← Added
role: text("role", { enum: ["admin", "user", "moderator"] }).notNull().default("user"),
// ...
});
npx drizzle-kit generate
# Generates: drizzle/0001_add_user_bio.sql
-- drizzle/0001_add_user_bio.sql (auto-generated)
ALTER TABLE "users" ADD COLUMN "bio" text;
ALTER TABLE "users" ADD COLUMN "avatar_url" text;
Introspecting an Existing Database
# Pull TypeScript schema from existing database
npx drizzle-kit introspect
# Creates src/db/schema.ts from your live database
Atlas: Schema-as-Code with Drift Detection
Atlas takes a declarative approach — you define your desired schema state, and Atlas figures out the migration SQL needed to reach it. Its killer feature is drift detection for CI/CD.
Installation
# macOS
brew install ariga/tap/atlas
# Linux / CI
curl -sSf https://atlasgo.sh | sh
Schema Definition (HCL)
# schema.hcl — declarative schema definition
schema "public" {}
table "users" {
schema = schema.public
column "id" {
type = uuid
default = sql("gen_random_uuid()")
}
column "email" {
type = text
null = false
}
column "name" {
type = text
null = false
}
column "role" {
type = enum("admin", "user", "moderator")
default = "user"
null = false
}
column "created_at" {
type = timestamptz
default = sql("NOW()")
null = false
}
primary_key {
columns = [column.id]
}
index "users_email_unique" {
columns = [column.email]
unique = true
}
}
Alternatively: SQL Schema
-- schema.sql — Atlas also accepts plain SQL schema files
CREATE TABLE "users" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'user',
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT "users_pkey" PRIMARY KEY ("id"),
CONSTRAINT "users_email_unique" UNIQUE ("email")
);
Atlas Config
# atlas.hcl
variable "database_url" {
type = string
default = getenv("DATABASE_URL")
}
env "local" {
src = "file://schema.hcl"
url = var.database_url
dev = "docker://postgres/16/dev?search_path=public" # Ephemeral dev DB for diffing
}
env "production" {
src = "file://schema.hcl"
url = var.database_url
}
Plan and Apply Migrations
# Show what SQL Atlas will execute (dry run)
atlas schema apply --env local --dry-run
# Apply schema changes
atlas schema apply --env local
# Check for drift — fail if DB doesn't match schema
atlas schema diff --env local
# exits 0 if no drift, 1 if drift detected
# Generate a versioned migration file
atlas migrate diff add_user_bio \
--dir "file://migrations" \
--to "file://schema.hcl" \
--dev-url "docker://postgres/16/dev"
# Apply versioned migrations
atlas migrate apply --env local
CI/CD Drift Detection
# .github/workflows/schema-check.yml
name: Schema Check
on:
pull_request:
paths:
- "schema.hcl"
- "migrations/**"
jobs:
check-schema:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- name: Install Atlas
uses: ariga/setup-atlas@v0
- name: Check for schema drift
run: |
atlas migrate validate \
--dir "file://migrations" \
--dev-url "postgres://postgres:postgres@localhost:5432/testdb"
- name: Lint migrations (destructive check)
run: |
atlas migrate lint \
--dir "file://migrations" \
--dev-url "postgres://postgres:postgres@localhost:5432/testdb" \
--latest 1
Schema Visualization
# Generate ERD diagram from your schema
atlas schema inspect \
--url "postgres://..." \
--format "{{ mermaid . }}"
# Output: Mermaid diagram of your tables and relationships
dbmate: Simple SQL Migration Runner
dbmate is a zero-magic migration runner — plain SQL files, versioned by timestamp, with -- migrate:up and -- migrate:down sections. Works with any language/framework.
Installation
# macOS
brew install dbmate
# Linux / CI
curl -fsSL -o /usr/local/bin/dbmate \
https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64
chmod +x /usr/local/bin/dbmate
# Or via npm (useful for Node projects)
npm install --save-dev dbmate
Creating Migrations
# Create a new migration file
dbmate new create_users_table
# Creates: db/migrations/20260309120000_create_users_table.sql
-- db/migrations/20260309120000_create_users_table.sql
-- migrate:up
CREATE TABLE users (
id UUID NOT NULL DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT users_email_unique UNIQUE (email)
);
CREATE INDEX users_email_idx ON users (email);
-- migrate:down
DROP TABLE IF EXISTS users;
-- db/migrations/20260309120001_create_posts_table.sql
-- migrate:up
CREATE TABLE posts (
id UUID NOT NULL DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
slug TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN NOT NULL DEFAULT FALSE,
view_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT posts_pkey PRIMARY KEY (id),
CONSTRAINT posts_slug_unique UNIQUE (slug)
);
CREATE INDEX posts_author_idx ON posts (author_id);
CREATE INDEX posts_published_idx ON posts (published, created_at);
-- migrate:down
DROP TABLE IF EXISTS posts;
Running Migrations
# Set database URL
export DATABASE_URL="postgres://user:pass@localhost:5432/myapp"
# Apply all pending migrations
dbmate up
# Rollback one migration
dbmate down
# Apply exactly one migration
dbmate up --one
# Show migration status
dbmate status
# Show current schema dump
dbmate dump
# Rollback and reapply latest migration (for development)
dbmate rollback && dbmate up --one
dbmate in package.json
{
"scripts": {
"db:migrate": "dbmate up",
"db:rollback": "dbmate down",
"db:status": "dbmate status",
"db:new": "dbmate new",
"db:reset": "dbmate drop && dbmate create && dbmate up"
}
}
Using dbmate in CI/CD
# .github/workflows/test.yml
- name: Run migrations
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb
run: |
curl -fsSL -o /usr/local/bin/dbmate \
https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64
chmod +x /usr/local/bin/dbmate
dbmate up
Feature Comparison
| Feature | Drizzle-Kit | Atlas | dbmate |
|---|---|---|---|
| Language | TypeScript | HCL / SQL | Any (CLI) |
| Migration generation | ✅ Auto from ORM schema | ✅ Auto diff | ❌ Manual SQL |
| Drift detection | ❌ | ✅ | ❌ |
| ORM integration | ✅ Drizzle ORM | ❌ | ❌ |
| Plain SQL files | Generated SQL | ✅ | ✅ |
| Rollback | SQL files | ✅ | ✅ explicit |
| CI/CD lint | ❌ | ✅ destructive check | ❌ |
| Schema viz (ERD) | ❌ | ✅ | ❌ |
| DB introspection | ✅ | ✅ | ❌ |
| Multi-DB support | PG, MySQL, SQLite, LibSQL | PG, MySQL, SQLite, MSSQL, ClickHouse | PG, MySQL, SQLite, MSSQL |
| Dev dependency size | Medium | Standalone binary | Standalone binary |
| Learning curve | Low (if using Drizzle) | Medium | Very low |
| GitHub stars | 27k (Drizzle ORM) | 6.4k | 3.1k |
When to Use Each
Choose Drizzle-Kit if:
- You're already using Drizzle ORM — it's the only migration tool that auto-generates from TypeScript schema definitions
- You want TypeScript types and migrations from a single source of truth
- Database introspection of an existing DB into TypeScript is needed
- You want an interactive UI (
drizzle-kit studio) to visualize and edit your schema
Choose Atlas if:
- Schema drift detection in CI/CD is a requirement (fail PRs if DB doesn't match schema)
- Destructive migration linting (catch column drops/table truncates before production)
- You work with multiple database engines and want a consistent migration tool
- ERD visualization of your schema is useful for the team
- Your team isn't on Drizzle ORM but wants declarative schema management
Choose dbmate if:
- Your team already writes SQL and prefers full control over migration files
- Language-agnostic tooling is important (Go/Python/Ruby services sharing the same DB)
- Simplicity is paramount —
up/downSQL,schema_migrationstable, done - You're migrating from Rails or any other framework with a similar migration pattern
Ecosystem and Community
Drizzle-Kit is inseparable from Drizzle ORM, which has grown to 27,000 stars and become one of the fastest-growing TypeScript ORM libraries. The drizzle team ships Drizzle-Kit as part of the ORM ecosystem — whenever Drizzle ORM adds a new feature (a new column type, a new index option), Drizzle-Kit gets updated to generate the corresponding migration SQL. The Drizzle Discord (30,000+ members) is the primary support channel, and the team is highly responsive to issues. Drizzle-Kit Studio (the interactive schema viewer) is a genuinely useful tool for visualizing your database schema during development.
Atlas was created by Ariga, a company founded by developers with deep infrastructure backgrounds (including former HashiCorp team members). The Atlas CLI reached v1.0 in 2023 and has been used in production by companies across the database ecosystem. Atlas's CI/CD integration — the ariga/setup-atlas GitHub Action and the atlas migrate lint command — represents the most mature approach to automated migration safety of the three tools. The Atlas Cloud (managed service) extends the open-source CLI with a UI for visualizing migration history and schema changes across environments.
dbmate is a solo-maintained project by Andrew Macneil that has found a loyal following because of its deliberate simplicity. With 3,100 stars and no external dependencies, it does one thing well: manage versioned SQL migration files. The project's longevity (started in 2016) and continued maintenance despite its small scope reflects how many teams want exactly what it offers — nothing more, nothing less. Language-agnostic tooling is rare in the migration space, and dbmate's single binary distribution makes it easy to use in polyglot microservice environments.
Real-World Adoption
Drizzle-Kit is the standard choice for any project using Drizzle ORM, which describes a significant and growing portion of the Node.js database ecosystem. Companies building with Next.js, Remix, or other TypeScript-first frameworks often choose Drizzle for its TypeScript-native design, and Drizzle-Kit comes along as the natural migration tool. The integration with Neon, PlanetScale, and Turso (all popular serverless databases) means Drizzle-Kit sees heavy usage in serverless and edge deployments where the schema management tooling needs to be CI/CD-friendly. For more on these serverless database platforms, see Neon vs Supabase vs Tembo serverless Postgres 2026.
Atlas is particularly valued in organizations with a dedicated platform or DevOps team. The drift detection capability — running atlas schema diff in CI to catch cases where a developer manually modified the production database without a migration — addresses a real production incident category. Financial services companies and healthcare platforms where database correctness is critical have adopted Atlas specifically for this capability. The destructive migration linting (atlas migrate lint catching column drops before they reach production) has prevented production incidents at companies where migration review was previously informal.
dbmate is the migration tool of choice in polyglot environments. A startup might have a Node.js API, a Python data pipeline, and a Go background worker — all sharing the same PostgreSQL database. Managing migrations with a language-specific ORM would require each language's migration tool to stay in sync. dbmate's pure-SQL approach means all three services can use the same migration files and the same CLI binary. This use case is where dbmate genuinely excels over ORM-coupled alternatives.
Developer Experience Deep Dive
Drizzle-Kit's DX is best understood in the context of the full Drizzle ORM experience. You define your schema once in TypeScript, get full IDE autocomplete for schema modifications, and drizzle-kit generate produces the migration SQL. The meta-directory approach (storing schema snapshots alongside migrations) means the diff is based on your last generated state rather than a live database — which means migrations generate correctly even in CI environments where the database may not be accessible.
The primary DX challenge is understanding when Drizzle-Kit's diff is based on the meta-snapshot vs. the live database. drizzle-kit generate uses the snapshot; drizzle-kit push applies directly to the database for development (bypassing migrations). This distinction trips up developers who use push in development and then expect generate to detect those same changes — the snapshot and the live DB can diverge if push is used outside of migrations.
Atlas's DX centers around the dev database concept — Atlas spins up an ephemeral Docker container with a fresh database to diff your schema against, rather than diffing against your actual development database. This is architecturally clean (no dependency on a running development database) but requires Docker and adds a few seconds to each schema operation. The Atlas Cloud integration adds a visual timeline of schema changes that teams find useful for understanding migration history during incidents.
dbmate's DX is the simplest of the three by design. dbmate new <name> creates a timestamped file, you write SQL, dbmate up runs it. The migration status command (dbmate status) shows exactly which migrations have run and which haven't. There's no configuration beyond the DATABASE_URL environment variable. The learning curve is essentially zero for anyone who has written SQL. The trade-off is that you get no automation — every schema change requires a manually written SQL file.
Migration Guide
Adopting Drizzle-Kit in a project with existing migrations starts with drizzle-kit introspect to generate a TypeScript schema from your existing database. Review and clean up the generated schema (introspection isn't perfect for every column type), then commit it as the baseline. Future schema changes are made in TypeScript, and drizzle-kit generate produces incremental migrations from that point forward.
Adding Atlas to a project with existing dbmate migrations is additive — Atlas can manage migrations in the same format as dbmate's plain SQL files. Point Atlas at your existing migrations directory with --dir "file://db/migrations" and use atlas migrate validate to verify they're internally consistent. You can then add Atlas's CI/CD checks (atlas migrate lint) on top of your existing dbmate workflow without changing the migration files themselves.
Migrating from Prisma Migrate to Drizzle-Kit is a path some teams take when adopting Drizzle ORM. Export your current Prisma schema, convert it to Drizzle's TypeScript schema syntax (there are community-maintained converters), and use drizzle-kit introspect on your current database to verify the generated schema matches. Existing Prisma migrations can be retained as-is in the database's migration history — Drizzle-Kit will start tracking from the current state forward.
Final Verdict 2026
For TypeScript projects using Drizzle ORM, Drizzle-Kit is the obvious and only choice — no other tool generates migrations from Drizzle schema definitions. The TypeScript-to-SQL workflow is clean, the generated SQL is correct, and the studio UI adds genuine value for schema visualization. Use drizzle-kit studio during development and drizzle-kit migrate in CI/CD.
Atlas earns its place in mature engineering organizations where database safety is a priority. The drift detection and destructive migration linting capabilities are unique among these three tools and represent a real advance in migration safety. If your team has ever had a production incident caused by schema drift or an accidentally destructive migration, Atlas's CI/CD integration is worth the additional configuration complexity.
dbmate's value is its simplicity and language-agnosticism. For small teams that write SQL fluently, polyglot environments, or projects that need migration tooling without framework coupling, dbmate is the pragmatic choice. Its 3,100 stars across 8 years of existence reflect a sustained niche rather than hype — the teams that use dbmate tend to stay with it.
Methodology
Data sourced from official Drizzle-Kit documentation (orm.drizzle.team/kit-docs), Atlas documentation (atlasgo.io), dbmate GitHub repository and documentation, GitHub star counts as of February 2026, and community discussions in the Drizzle Discord and the Atlas community Slack.
Related: pgvector vs Qdrant vs Weaviate Vector Databases 2026, Turso vs PlanetScale vs Neon Serverless Database 2026, Best Next.js Auth Solutions 2026
Also related: Drizzle ORM vs Prisma vs TypeORM for the ORM layer that sits above these migration tools, or Neon vs Supabase Postgres vs Tembo for the PostgreSQL hosting platforms these migrations run against.