Skip to main content

Migrations

Migrations let you evolve your database schema and seed data over time. They're named, reversible, and automatically tracked.

Creating a Migration​

import { createMigration } from '@sajjadbzn/jcell'

const m001 = createMigration('001_create_users', {
async up(db) {
// Define the schema
const userSchema = schema({
id: t.id(),
name: t.string(),
email: t.string().index({ unique: true }),
role: t.enum(['admin', 'user'] as const),
createdAt: t.date().default(() => new Date()),
})

// Register the collection (this creates the table/indexes)
db.collection('users', userSchema)

// Seed initial data
const users = db.collection('users', userSchema)
await users.insert({ name: 'Admin', email: 'admin@example.com', role: 'admin' })
},

async down(db) {
// Rollback (e.g., drop collection data)
// Note: this is optional — you can omit it if rollback isn't needed
},
})

Running Migrations​

const db = createDB({ adapter: sqliteAdapter({ path: './app.db' }) })

await db.migrate([m001, m002, m003])
  • Only unapplied migrations are executed
  • Applied migrations are tracked in the _migrations collection
  • Order is determined by the array — run earlier migrations first

How It Works​

  1. jcell creates a _migrations collection with schema:
    { id: string, name: string, appliedAt: Date }
  2. On db.migrate([...]), jcell queries _migrations to find already-applied migrations
  3. For each unapplied migration, it calls migration.up(db) and records the name
  4. If migration.down() is defined, it can be used to roll back

Complete Example​

import { createDB, createMigration, schema, t, sqliteAdapter } from '@sajjadbzn/jcell'

// Migration 001: Create users table
const m001 = createMigration('001_create_users', {
async up(db) {
const userSchema = schema({
id: t.id(),
name: t.string(),
email: t.string().index({ unique: true }),
role: t.enum(['admin', 'user'] as const),
})
db.collection('users', userSchema)

const users = db.collection('users', userSchema)
await users.insert({ name: 'Admin', email: 'admin@test.com', role: 'admin' })
},
})

// Migration 002: Add profile data
const m002 = createMigration('002_add_profiles', {
async up(db) {
const profileSchema = schema({
id: t.id(),
userId: t.ref('users'),
bio: t.string().optional(),
avatar: t.string().optional(),
})
db.collection('profiles', profileSchema)
},
})

// Run migrations
const db = createDB({ adapter: sqliteAdapter({ path: './app.db' }) })
await db.migrate([m001, m002])

console.log('Migrations complete! ✅')

Best Practices​

  1. Name migrations meaningfully — use descriptive names like 001_initial_schema or 002_add_email_index
  2. Make migrations idempotent — use db.collection() which returns existing collections if already registered
  3. Always define up — the migration must apply forward
  4. Define down for rollbacks — helpful during development
  5. Run migrations in order — the array order determines execution order
  6. Use adapters that persist — the _migrations collection needs persistence (SQLite, file)
note

Migrations work with any adapter that persists data (file, SQLite, D1). For the memory adapter, migrations are re-run on every restart since the _migrations collection is ephemeral.