Migrations
Migrations are versioned TypeScript files that build and evolve your D1 schema. Each file exports an up (apply) and down (revert) function that receive a Schema instance — no db argument, no raw SQL execution. The CLI tracks which migrations have run in a _migrations table.
The full migration DSL — every column type, constraint, foreign key, and modifier — lives in the Schema Builder reference. This page covers the day-to-day workflow; reach for that page when you need the exact method signatures.
Workflow at a glance
bunx d1-eloquent make:migration create_users_table # 1. scaffold
# …edit the generated file…
bunx d1-eloquent migrate # 2. apply pending
bunx d1-eloquent status # 3. inspect state
bunx d1-eloquent rollback # 4. revert last batch
bunx d1-eloquent fresh # 5. drop all + re-runThe binding name is read from wrangler.jsonc automatically and defaults to local D1 — no --db/--local flags needed.
1. Scaffold a migration
bunx d1-eloquent make:migration create_users_tableThis writes src/database/migrations/TIMESTAMP_create_users_table.ts with commented stubs. One migration per domain entity — keep them small and focused rather than monolithic. See the Generators reference for all make:* commands.
2. Write the schema
// src/database/migrations/20260101000000_create_users_table.ts
import type { TMigration } from '@orphnet/d1-eloquent/cli'
import { Schema } from '@orphnet/d1-eloquent/cli'
const migration: TMigration = {
name: '20260101000000_create_users_table',
up: (schema: Schema) => {
schema.createTable('users', (t) => {
t.id() // TEXT PRIMARY KEY (UUID)
t.text('name')
t.text('email', { unique: true })
t.boolean('is_admin').default(false)
t.text('settings').nullable() // JSON stored as TEXT
t.timestamps() // created_at / updated_at
t.softDeletes() // deleted_at (omit if not soft-deleting)
})
},
down: (schema: Schema) => {
schema.dropTable('users')
},
}
export default migrationForeign keys, composite keys, indexes, check constraints, and altering existing tables are all covered in the Schema Builder reference.
Keep down a true inverse
Whatever up creates, down should undo — so rollback and fresh stay clean. For a createTable, that's a matching dropTable; for an addColumn alter, drop the column.
3. Apply migrations
bunx d1-eloquent migrateRuns every pending migration in timestamp order and records each in _migrations:
Running: 20260101000000_create_users_table
✓ Applied 1 migrationSee the migrate reference for remote application and dry-run options.
4. Roll back
bunx d1-eloquent rollbackReverts the most recent batch by calling each migration's down. See the rollback reference.
5. Fresh rebuild
bunx d1-eloquent fresh # drop all tables, re-run every migration
bunx d1-eloquent fresh --seed # …then run seedersfresh drops tables in FK-safe topological order, then re-applies the full migration history — ideal for resetting local dev. See the fresh reference.
Generate migrations from your models
generate diffs each model against the migrations already on disk and writes a reconciling migration for you — no hand-writing ALTER TABLE. It's pure static analysis: it reads your model files and migration files and never touches a database. The emitted file is your review gate before migrate applies it.
bunx d1-eloquent generate # dry run — print the diff for every model
bunx d1-eloquent generate Post # just one model
bunx d1-eloquent generate Post --write # emit the migration file
bunx d1-eloquent generate Post --write --name=add_pinned_to_posts # custom name--name=<name> overrides the derived create_<table> / update_<table> name segment (the timestamp prefix is always kept). It only applies when exactly one migration file is emitted - a multi-table run keeps the derived per-table names.
The desired schema is derived from the model itself: its attribute-type fields (the TS type/interface passed to BaseModel<…>), refined by static casts (boolean → INTEGER, json/array → JSON, date → TEXT, …), plus the conventional created_at/updated_at (when timestamps) and deleted_at (when softDeletes). That's diffed against the columns your migrations already declare for the table.
// You add two fields to the model…
interface PostAttrs {
// …existing fields…
pinned?: boolean // optional → nullable; boolean cast → INTEGER
subtitle: string | null
}bunx d1-eloquent generate Post --write
# ~ Post (alter "posts")
# + pinned INTEGER nullable
# + subtitle TEXT
# ✓ Wrote database/migrations/20260707201912_update_posts.ts// …and get a ready-to-run migration (up adds, down reverses):
up: (schema) => schema.table("posts", (t) => {
t.addInteger("pinned", { nullable: true })
t.addText("subtitle")
}),
down: (schema) => schema.table("posts", (t) => {
t.dropColumn("pinned")
t.dropColumn("subtitle")
}),A brand-new model (no migration for its table yet) generates a full createTable instead of an alter.
Review before you migrate
generate emits a file — it does not apply anything. Always read it first.
- Dropped columns are destructive and are annotated with a
⚠️comment. Removing a field from a model makesgeneratepropose adropColumnthat deletes that column's data. - SQLite can't
ALTERa column's type in place, so a storage-class change (e.g. TEXT → INTEGER) is reported as a warning, never auto-emitted — recreate the table manually if you need it. - Declared-column read-back honours prior alter migrations: columns added via
t.addText()/addInteger()/… and removed viat.dropColumn()in a migration'sup()are tracked, sogenerateround-trips its own output and won't re-propose an already-added column.
New migrations are written to wherever your existing migrations live (falling back to src/database/migrations for a greenfield project).
Next steps
- Schema Builder reference — every column type, constraint, and modifier
- Seeders & Factories — populate the schema with realistic data
migrate·rollback·fresh— full CLI flags