Migrations
Rudder unifies migration commands across all three engines — the native engine's first-party migration runner and the Prisma/Drizzle adapters' tooling. The same pnpm migrate works everywhere; the active engine is auto-detected.
pnpm migrate # apply pending migrations
pnpm migrate:fresh # drop everything and re-run
pnpm migrate:status # show what's applied
pnpm db:seed # run database/seeders/DatabaseSeederOn the native engine these run the built-in runner over database/migrations/ — plus migrate:rollback, migrate:reset, and migrate:refresh, which exist only there (see Rollbacks), and --connection=<name> / --path=<dir> for multi-database apps. A successful run also regenerates the typed model registry.
On Prisma/Drizzle, the commands delegate to the underlying tool:
| Command | Prisma (dev) | Prisma (prod) | Drizzle |
|---|---|---|---|
migrate |
prisma migrate dev |
prisma migrate deploy |
drizzle-kit migrate |
migrate:fresh |
prisma migrate reset --force |
same | drizzle-kit migrate --force |
migrate:status |
prisma migrate status |
same | drizzle-kit check |
make:migration <name> |
prisma migrate dev --create-only --name <name> |
— | drizzle-kit generate --name <name> |
db:push |
prisma db push |
same | drizzle-kit push |
db:generate |
prisma generate |
same | (no-op) |
db:seed |
run database/seeders/DatabaseSeeder.ts |
same | same |
db:push and db:generate are Prisma/Drizzle-only — the native engine has no push mode and no client to generate; every change is a tracked migration.
make:migration also takes a --vector <table> <column> <dimensions> flag (optional --metric cosine|l2|inner-product, default cosine) that scaffolds a pgvector migration — CREATE EXTENSION, the vector(N) column, and an HNSW index — in your ORM's migration format. Postgres only; see the vector({ dimensions }) cast under Models — Casts for the read/write side.
Production vs development is selected by NODE_ENV. In production, migrate runs prisma migrate deploy (apply only — never generate, never prompt) instead of migrate dev.
You can also run any of these via the rudder CLI directly: pnpm rudder migrate, pnpm rudder db:seed, etc.
Adding a new table
The flow has engine-specific steps (describe the table) and one shared step (write the model).
Native
# 1. Create + edit the migration
pnpm rudder make:migration create_posts_table
# 2. Apply — also regenerates the typed registry
pnpm rudder migrate
# 3. Create the Model class
pnpm rudder make:model PostFill in the generated stub with the Laravel-style blueprint:
// database/migrations/<timestamp>_create_posts_table.ts
import { Migration, Schema } from '@rudderjs/database'
export default class extends Migration {
async up() {
await Schema.create('posts', (t) => {
t.id()
t.string('title')
t.text('body').nullable()
t.timestamps()
})
}
async down() {
await Schema.dropIfExists('posts')
}
}Bind the model with Model.for<'posts'>() and the column types come from the migrated schema — no hand-declared fields. See the Native Engine guide for the full blueprint surface.
Prisma
# 1. Edit prisma/schema/app.prisma — add the model
# 2. Apply
pnpm db:push # dev: fast diff, no migration file
# OR
pnpm rudder make:migration create_posts_table
pnpm migrate # production-track: versioned SQL
# 3. Generate the client
pnpm rudder db:generate
# 4. Create the Model class
pnpm rudder make:model PostEdit prisma/schema/app.prisma:
model Post {
id String @id @default(cuid())
title String
body String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}::: tip make:model --migration
Combine steps with pnpm rudder make:model Post --migration — it creates the Model class and an empty create_posts_table migration in one shot. You still edit the Prisma schema yourself.
:::
Drizzle
# 1. Edit db/schema.ts — add the table
# 2. Generate the migration SQL (Drizzle diffs the schema for you)
pnpm rudder make:migration create_posts_table
# 3. Apply
pnpm migrate
# 4. Create the Model class
pnpm rudder make:model PostEdit db/schema.ts:
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const posts = sqliteTable('posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
body: text('body'),
createdAt: text('created_at').notNull(),
updatedAt: text('updated_at').notNull(),
})Adding a column to an existing table
Same flow as creating a table — describe the change, then apply it. The make:model step is skipped (the model already exists).
# Native
pnpm rudder make:migration add_excerpt_to_posts
# → up(): await Schema.table('posts', (t) => { t.string('excerpt').nullable() })
pnpm rudder migrate
# Models bound with Model.for<'posts'>() pick up the new column's type
# automatically — the registry regenerated with the migrate.
# Prisma
# 1. Add `excerpt String?` to the Post model in app.prisma
pnpm db:push # dev
# OR
pnpm rudder make:migration add_excerpt_to_posts
pnpm migrate # versioned
# 2. Add `excerpt!: string | null` to app/Models/Post.ts
# Drizzle
# 1. Add `excerpt: text('excerpt')` to the posts table in schema.ts
pnpm rudder make:migration add_excerpt_to_posts # auto-diffs
pnpm migrate
# 2. Add `excerpt!: string | null` to app/Models/Post.tsdb:push vs migrate
Two workflows on Prisma/Drizzle. Pick one per project:
db:push— fast prototype mode. Diffs your schema against the live database and applies the changes immediately. No migration file. No version history. Dev only.migrate— production track. Records every change as a versioned file (prisma/migrations/,drizzle/migrations/, or the native engine'sdatabase/migrations/). Reviewable, replayable, deployable.
Use db:push while you're modeling. Switch to migrate once the schema stabilizes and you need a paper trail. The native engine is migrate-only by design — there's no push mode, so the paper trail is always there.
Seeders
Create database/seeders/DatabaseSeeder.ts:
import { Seeder } from '@rudderjs/orm'
import { User } from '../../app/Models/User.js'
import { Post } from '../../app/Models/Post.js'
export default class DatabaseSeeder extends Seeder {
async run(): Promise<void> {
await User.create({ name: 'Alice', email: 'alice@example.com', role: 'admin' })
await User.create({ name: 'Bob', email: 'bob@example.com', role: 'user' })
await Post.create({ title: 'First post', userId: 'alice' })
}
}Run it:
pnpm db:seedFor larger projects, split into separate seeders and call them from DatabaseSeeder:
import { Seeder } from '@rudderjs/orm'
import { UserSeeder } from './UserSeeder.js'
import { PostSeeder } from './PostSeeder.js'
export default class DatabaseSeeder extends Seeder {
async run(): Promise<void> {
await this.call([UserSeeder, PostSeeder])
}
}// database/seeders/UserSeeder.ts
import { Seeder } from '@rudderjs/orm'
import { User } from '../../app/Models/User.js'
export class UserSeeder extends Seeder {
async run(): Promise<void> {
await User.create({ name: 'Alice', email: 'alice@example.com' })
}
}The db:seed runner accepts either a class extending Seeder (instantiates and calls .run()) or a plain async function (invokes it directly).
If you'd rather use Prisma's native seed integration, configure prisma.seed in package.json and skip the DatabaseSeeder.ts file — db:seed falls back to prisma db seed automatically.
Rollbacks
Rollback support depends on whether the engine's migrations carry a down() method:
- Native engine — full Laravel parity. Migrations are classes with explicit
up()/down(), somigrate:rollback(revert the last batch),migrate:reset(revert everything), andmigrate:refresh(reset + re-run) all work, and batches are transactional where the driver supports it. - Prisma / Drizzle — forward-only SQL files, no
down(), so those commands don't exist there. In dev,pnpm migrate:freshdrops everything and re-runs all migrations. In production, write a new forward migration that reverses the change — the same advice Prisma's docs give for "rolling back" a deployed migration.
Common pitfalls
No ORM detected— the migrate commands look for the native engine (engine: 'native'on the default connection inconfig/database.ts) or an installed@rudderjs/orm-prisma/@rudderjs/orm-drizzle. Make sure one of the three is actually wired.db:pushin production — don't. It bypasses the migrations folder and can silently drop columns. Usemigratefor anything you ship.- Stale Prisma client — after editing
schema.prisma, runpnpm rudder db:generate(or it'll happen automatically as part ofmigrate) before importing new fields in TypeScript. - Drizzle
make:migrationproduces an empty SQL file — that means yourdb/schema.tsis identical to the last applied snapshot. Edit the schema first, then runmake:migration.