DEV Community

teaganga
teaganga

Posted on

How to Seed Data in Drizzle (The Right Way)

Best practices for Drizzle ORM, migrations, seed scripts, and system data

Seeding data is one of the most common tasks when building a backend—but if you're using Drizzle ORM, the right way to seed data may not be obvious at first.

Should you put seed data inside migrations?
Should you generate seed migrations automatically?
Should you write seed scripts instead?
What goes where?

This guide breaks down exactly how to seed data in Drizzle the right way, along with example SQL and TypeScript migrations.


⭐ Why Seeding Data Matters

Seeding is how you insert:

  • Essential system constants (roles, permissions, configuration defaults)
  • Initial lookup tables
  • Local development data
  • Demo or sample data

Drizzle handles schema migrations beautifully—but when it comes to seeding, there are a few rules that help you avoid headaches across environments.


1. Two Ways to Seed Data in Drizzle

Drizzle supports seeding in two primary ways:

A) Seed data in a migration (SQL or TypeScript)

→ Best for system-required data like roles, permissions, default settings.

B) Seed data using a seed script (your own script)

→ Best for environment-specific data like test users, fake orgs, or demo content.

Understanding the difference is important.


2. When You Should Seed Data in Migrations

Migrations should include static, deterministic, environment-independent data that your application must have to function.

Examples:

  • Default RBAC roles
  • Permissions
  • Role → permission mappings
  • Default feature flags
  • System-level configuration rows
  • Enum-like lookup tables

These values belong in migrations because they define the system itself.

✔️ Good migration seed examples

INSERT INTO roles (id, name) VALUES
  ('admin', 'Administrator'),
  ('editor', 'Editor')
ON CONFLICT (id) DO NOTHING;
Enter fullscreen mode Exit fullscreen mode

These values never change based on environment—so they belong in migrations.


3. When You Should Not Seed Data in Migrations

Never put environment-specific or temporary data inside migrations.

These do NOT belong in migrations:

  • Test users
  • Demo accounts
  • Sample websites
  • Development-only data
  • API keys or secrets
  • Customer, agency, or tenant data

Migrations must remain stable, repeatable, and safe to run in production.

So anything environment-specific should go into a seed script, not a migration.


4. Seeding Data Inside Drizzle Migrations

You can seed data using:

Option 1 — SQL migration (.sql)

Drizzle runs plain SQL migrations out of the box.

Example: 0005_seed_roles.sql

INSERT INTO rbap_roles (id, scope, name) VALUES
  ('platform_admin', 'platform', 'Platform Admin'),
  ('partner_admin', 'partner', 'Partner Admin'),
  ('customer_admin', 'customer', 'Customer Admin')
ON CONFLICT (id) DO NOTHING;
Enter fullscreen mode Exit fullscreen mode

Option 2 — TypeScript migration (.ts)

If you prefer Drizzle's typed database client (and sql tag):

import { sql } from "drizzle-orm";

export const up = async (db) => {
  await db.execute(sql`
    INSERT INTO rbap_roles (id, scope, name) VALUES
      ('platform_admin', 'platform', 'Platform Admin'),
      ('partner_admin', 'partner', 'Partner Admin'),
      ('customer_admin', 'customer', 'Customer Admin')
    ON CONFLICT (id) DO NOTHING;
  `);
};

export const down = async (db) => {
  await db.execute(sql`
    DELETE FROM rbap_roles 
    WHERE id IN ('platform_admin', 'partner_admin', 'customer_admin');
  `);
};
Enter fullscreen mode Exit fullscreen mode

Both options work perfectly.


5. Does Drizzle Auto-Generate Seed Migrations?

❌ No.

Drizzle generates schema change migrations only.

That includes:

  • create table
  • add column
  • drop index
  • change constraint

Drizzle does not generate migrations for:

  • inserts
  • seed data
  • role lists
  • permissions
  • enums implemented via lookup tables

So you must write these manually (SQL or TS).


6. Creating a Seed Script for Local or Dev Data

For data you want in development but not in production, create your own simple script:

Example: seed.ts

import { db } from "./db";
import { users } from "./schema";

await db.insert(users).values({
  id: "dev-user-1",
  email: "test@example.com",
  name: "Dev Tester",
});
Enter fullscreen mode Exit fullscreen mode

Run it with:

ts-node seed.ts
Enter fullscreen mode Exit fullscreen mode

or add a script:

"scripts": {
  "seed": "ts-node seed.ts"
}
Enter fullscreen mode Exit fullscreen mode

This keeps dev/test data separate from production migrations.


7. Best Practices for Seeding in Drizzle

  • Put system-level constants inside migrations: Roles, permissions, status codes, lookup tables.

  • Put environment-specific data inside seed scripts: Demo data, test users, dev orgs.

  • Never mix schema migrations with app data: Avoid creating partner agencies, customer orgs, or users inside migrations.

  • Use ON CONFLICT DO NOTHING for idempotency: Ensures migrations stay safe and re-runnable.

  • Keep migrations deterministic: Every run should produce the same final result.


Final Takeaway

Seeding in Drizzle is extremely flexible—once you follow the right boundaries.

Task Migration Seed Script
Create roles ✅ Yes ❌ No
Create demo users ❌ No ✅ Yes
Insert default permissions ✅ Yes ❌ No
Insert dev-only test data ❌ No ✅ Yes
Populate lookup tables ✅ Yes ❌ No

If the data defines the system, put it in a migration.
If the data defines the environment, put it in a seed script.

Top comments (0)