Skip to main content
Back to Blog
Tutorials
3 min read
November 27, 2024

How to Implement Database Migrations with Drizzle ORM

Set up database migrations with Drizzle ORM including schema changes, rollbacks, seeding, and CI/CD integration.

Ryel Banfield

Founder & Lead Developer

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 push for rapid prototyping, migrate for 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.

databasemigrationsDrizzle ORMPostgreSQLNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles