SQL-style migrations for Dexie: numbered files, snapshot, squashing, resumable upgrades, multi-tab safe.
Give Dexie apps a linear, testable, deterministic migration story—like SQL migrations—without fighting IndexedDB's constraints.
✅ Numbered, append-only migration files (like Rails/Django/Alembic)
✅ Deterministic schema evolution across teams and deployments
✅ Git-friendly (one file per change, easy to review)
✅ Resumable migrations (atomic steps, retry on failure)
✅ TypeScript-first with full type safety
✅ Framework-agnostic (works with React, Vue, Svelte, vanilla JS, etc.)
✅ Bundler-agnostic (Vite, Webpack, Rollup, or no bundler)
npm install dexie @dexie-kit/migratenpx dexie-migrate new "initial schema"This creates a migration file like migrations/0001_20251025143000_initial_schema.ts
// migrations/0001_20251025143000_initial_schema.ts
import type { Migration } from '@dexie-kit/migrate';
export default {
id: 1,
name: 'initial_schema',
stores: {
forms: 'id, name, createdAt',
responses: 'id, formId, createdAt'
}
} as Migration;// db/index.ts
import { runMigrations } from '@dexie-kit/migrate';
import m0001 from './migrations/0001_20251025143000_initial_schema';
const MIGRATIONS = [m0001];
export const db = await runMigrations('my-app-db', MIGRATIONS);// main.ts
import { db } from './db';
const forms = await db.forms.toArray();
console.log('Forms:', forms);Add a new migration:
npx dexie-migrate new "add user avatar field"Migration with data transformation:
// migrations/0002_20251025150000_add_updated_at.ts
import type { Migration } from '@dexie-kit/migrate';
export default {
id: 2,
name: 'add_updated_at',
stores: {
forms: 'id, name, createdAt, updatedAt'
},
async up(tx) {
const now = Date.now();
await tx.table('forms').toCollection().modify(form => {
form.updatedAt = form.updatedAt ?? now;
});
}
} as Migration;Basic usage:
import { runMigrations } from '@dexie-kit/migrate';
import m0001 from './migrations/0001_initial_schema';
import m0002 from './migrations/0002_add_updated_at';
const MIGRATIONS = [m0001, m0002];
const { db } = await runMigrations('my-app-db', MIGRATIONS);With options:
const { db, appliedMigrations, skippedMigrations } = await runMigrations(
'my-app-db',
MIGRATIONS,
{
verbose: true,
onProgress: (current, total) => {
console.log(`Migrating: ${current}/${total}`);
},
onError: (migration, error) => {
console.error(`Migration ${migration.id} failed:`, error);
},
onComplete: () => {
console.log('All migrations completed!');
}
}
);Vite:
const migrationModules = import.meta.glob<{ default: Migration }>(
'./migrations/*.ts',
{ eager: true }
);
const MIGRATIONS = Object.values(migrationModules)
.map(m => m.default)
.sort((a, b) => a.id - b.id);Webpack:
const migrationContext = require.context('./migrations', false, /\.ts$/);
const MIGRATIONS = migrationContext
.keys()
.map(key => migrationContext(key).default)
.sort((a, b) => a.id - b.id);Run migrations and return a Dexie database instance.
Parameters:
dbName(string): Database namemigrations(Migration[]): Array of migration objectsoptions(MigrationOptions, optional):dryRun(boolean): Preview without applyingverbose(boolean): Enable loggingonProgress(function): Progress callbackonError(function): Error callbackonComplete(function): Completion callback
Returns: Promise<MigrationResult>
db: Dexie database instanceappliedMigrations: IDs of newly applied migrationsskippedMigrations: IDs of already applied migrationsfinalVersion: Current database version
interface Migration {
id: number; // Required: unique ID
name: string; // Required: descriptive name
stores?: StoresMap; // Optional: schema changes
up?: (tx: Transaction) => Promise<void>; // Optional: data transformation
down?: (tx: Transaction) => Promise<void>; // Optional: test-only rollback
validateAfter?: (tx: Transaction) => Promise<boolean>; // Optional: validation
timeout?: number; // Optional: timeout in ms
}# Create new migration
npx dexie-migrate new "description"
# Check schema drift (coming soon)
npx dexie-migrate check
# Create snapshot (coming soon)
npx dexie-migrate snapshot
# Squash migrations (coming soon)
npx dexie-migrate squash --cutoff 20
# Print schema (coming soon)
npx dexie-migrate print-schema{
id: 3,
name: 'add_status',
stores: {
forms: 'id, name, createdAt, updatedAt, status'
},
async up(tx) {
await tx.table('forms').toCollection().modify(form => {
form.status = 'draft';
});
}
}{
id: 4,
name: 'index_forms_status',
stores: {
forms: 'id, name, createdAt, updatedAt, status' // status is now indexed
}
}{
id: 5,
name: 'rename_users_to_accounts',
stores: {
accounts: 'id, email, name',
users: null // mark for deletion
},
async up(tx) {
const oldData = await tx.table('users').toArray();
await tx.table('accounts').bulkAdd(oldData);
}
}- Dexie.js: ^3.0.0 || ^4.0.0
- Modern JavaScript environment: Browser with ES modules support or bundler
- Optional: TypeScript for type safety
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Safari iOS 14+
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Type check
npm run type-checkCreate and validate schema snapshots to detect drift:
# Create a snapshot of current schema
npx dexie-migrate snapshot
# Check for schema drift
npx dexie-migrate checkProgrammatic API:
import { createSnapshot, validateSchema, computeExpectedSchema } from '@dexie-kit/migrate';
// Create snapshot from database
const snapshot = await createSnapshot(db, migrations);
// Validate current schema against expected
const expected = computeExpectedSchema(migrations);
const result = validateSchema(snapshot, expected);
if (!result.valid) {
console.error('Schema errors:', result.errors);
}Combine multiple migrations into a single base migration:
# Squash migrations up to ID 20
npx dexie-migrate squash --cutoff 20
# Preview without making changes
npx dexie-migrate squash --cutoff 20 --dry-runProgrammatic API:
import { squashMigrations, renumberMigrations } from '@dexie-kit/migrate';
const result = squashMigrations(migrations, { cutoffId: 20 });
// result.baseMigration - the new combined migration
// result.remainingMigrations - migrations after cutoff
// result.squashedIds - IDs that were squashedEnsure safe migrations across multiple browser tabs:
import { runWithCoordination, MigrationCoordinator } from '@dexie-kit/migrate';
// Simple coordination wrapper
const result = await runWithCoordination(
'my-db',
async () => runMigrations('my-db', migrations),
{ lockTimeout: 30000, verbose: true }
);
// Or use coordinator directly for more control
const coordinator = new MigrationCoordinator('my-db', { verbose: true });
coordinator.on('migration_started', (msg) => {
console.log('Another tab started migration');
});
const locked = await coordinator.waitForLock();
if (locked) {
// Run migrations
coordinator.notifyMigrationStarted();
// ...
coordinator.notifyMigrationCompleted();
}
coordinator.destroy();Auto-import migrations in Vite projects:
// vite.config.ts
import { defineConfig } from 'vite';
import dexieMigrate from '@dexie-kit/migrate/vite-plugin';
export default defineConfig({
plugins: [
dexieMigrate({
migrationsDir: 'src/db/migrations',
validateSchema: true,
snapshotPath: '.dexie-migrate/snapshot.json'
})
]
});Then import migrations using a virtual module:
import migrations from 'virtual:dexie-migrate/migrations';
import { runMigrations } from '@dexie-kit/migrate';
const { db } = await runMigrations('my-db', migrations);Auto-import migrations in Webpack projects:
// webpack.config.js
const DexieMigratePlugin = require('@dexie-kit/migrate/webpack-plugin');
module.exports = {
plugins: [
new DexieMigratePlugin({
migrationsDir: 'src/db/migrations',
validateSchema: true
})
]
};Display migration progress to users:
Vanilla JavaScript:
import { showMigrationProgress } from '@dexie-kit/migrate/progress-ui';
const ui = showMigrationProgress('migration-container', {
title: 'Upgrading Database',
theme: 'dark'
});
await runMigrations('my-db', migrations, {
onProgress: (current, total) => {
ui.update({ current, total, status: 'running' });
}
});
ui.complete();React:
import { useMigrationProgress, MigrationProgress } from '@dexie-kit/migrate/react';
function App() {
const { progress, db } = useMigrationProgress({
dbName: 'my-db',
migrations: [m0001, m0002],
autoStart: true
});
if (!db) {
return <MigrationProgress progress={progress} />;
}
return <div>App ready!</div>;
}Vue 3:
<script setup>
import { MigrationProgress, useMigrationProgress } from '@dexie-kit/migrate/vue';
const { progress, db } = useMigrationProgress({
dbName: 'my-db',
migrations: [m0001, m0002],
autoStart: true
});
</script>
<template>
<MigrationProgress v-if="!db" :progress="progress" />
<div v-else>App ready!</div>
</template>- Core runtime (
runMigrations) - CLI (
newcommand) - Schema snapshot and validation
- Migration squashing
- Multi-tab coordination enhancements
- Vite/Webpack plugins
- Progress UI components
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
Inspired by migration systems from Rails, Django, and Alembic, adapted for IndexedDB and Dexie.js.