Database migrations keep your schema in sync with your code. Here is how to manage them with Drizzle ORM.
Step 1: Project Setup
pnpm add drizzle-orm @neondatabase/serverless
pnpm add -D drizzle-kit
Step 2: Drizzle Configuration
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema/*",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
});
Step 3: Define Your Schema
// src/db/schema/users.ts
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
avatarUrl: text("avatar_url"),
role: text("role", { enum: ["admin", "member", "viewer"] })
.notNull()
.default("member"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// src/db/schema/posts.ts
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
import { users } from "./users";
export const posts = pgTable("posts", {
id: text("id").primaryKey(),
title: text("title").notNull(),
slug: text("slug").notNull().unique(),
content: text("content").notNull(),
excerpt: text("excerpt"),
published: boolean("published").notNull().default(false),
authorId: text("author_id")
.references(() => users.id)
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
Step 4: Generate and Run Migrations
# Generate migration from schema changes
pnpm drizzle-kit generate
# Review what was generated
ls drizzle/
# Apply migrations
pnpm drizzle-kit migrate
# Or push schema directly (development only)
pnpm drizzle-kit push
Step 5: Database Connection
// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-serverless";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql, schema });
Step 6: Add a Column Migration
When you add a field to your schema:
// src/db/schema/users.ts — add a new column
export const users = pgTable("users", {
// ... existing columns ...
bio: text("bio"),
website: text("website"),
});
Then generate and apply:
pnpm drizzle-kit generate
# This creates: drizzle/0001_add_bio_website_to_users.sql
# Contents:
# ALTER TABLE "users" ADD COLUMN "bio" text;
# ALTER TABLE "users" ADD COLUMN "website" text;
pnpm drizzle-kit migrate
Step 7: Seed Script
// scripts/seed.ts
import { db } from "@/db";
import { users, posts } from "@/db/schema";
async function seed() {
console.log("Seeding database...");
// Clear existing data (development only)
await db.delete(posts);
await db.delete(users);
// Seed users
const [admin] = await db
.insert(users)
.values({
id: "user_1",
email: "admin@example.com",
name: "Admin User",
role: "admin",
})
.returning();
// Seed posts
await db.insert(posts).values([
{
id: "post_1",
title: "Welcome to the Blog",
slug: "welcome",
content: "This is the first post on our blog.",
excerpt: "Welcome!",
published: true,
authorId: admin.id,
},
{
id: "post_2",
title: "Getting Started Guide",
slug: "getting-started",
content: "Here is how to get started with our platform.",
excerpt: "A quick start guide.",
published: true,
authorId: admin.id,
},
]);
console.log("Seed complete!");
}
seed().catch(console.error);
Run with:
pnpm tsx scripts/seed.ts
Step 8: Package.json Scripts
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx scripts/seed.ts"
}
}
Step 9: CI/CD Integration
# .github/workflows/migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths:
- "drizzle/**"
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm db:migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Step 10: Drizzle Studio
Drizzle Studio provides a visual interface for exploring your database:
pnpm db:studio
This opens a browser-based tool where you can browse tables, run queries, and inspect data.
Best Practices
- Always review generated SQL before applying
- Never modify migration files after they have been applied
- Use
pushfor rapid prototyping,migratefor production - Keep migration files in version control
- Test migrations against a staging database before production
- Add indexes for frequently queried columns
Need Database Architecture Help?
We design and implement scalable database solutions for modern web applications. Contact us to discuss your project.