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;
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;
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');
`);
};
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",
});
Run it with:
ts-node seed.ts
or add a script:
"scripts": {
"seed": "ts-node seed.ts"
}
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 NOTHINGfor 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)