Auto-migrating Durable Objects
Durable Objects don't have a migration CLI. D1 has wrangler d1 migrations apply.
Postgres has prisma migrate. But a DO with embedded SQLite?
Each instance owns its own database. There's no central place to run ALTER TABLE.
The solution: migrations run on every wake-up. Before any business logic,
the DO checks a __migrations table, applies anything new,
and proceeds. A fresh DO runs all migrations. An existing one runs only what changed
since the last deploy. All inside transactionSync, so it's atomic.
The API
Migrations are a plain record with mNNNN keys. Import SQL files or inline them.
The runner validates they're sequential, splits multi-statement migrations on
--> statement-breakpoint, and tracks applied versions.
import { migrate } from "do-orm";
export class InboxDO {
constructor(state, env) {
migrate(state.storage, {
m0000: `CREATE TABLE messages (...)`,
m0001: `ALTER TABLE messages ADD COLUMN status TEXT`,
});
}
}
That's it. New DOs get both tables. Existing DOs with m0000 already applied
get only m0001. Failures roll back the entire transaction. The MigrationError
tells you exactly which version and statement broke.
Adopting from Drizzle
We built this after replacing Drizzle. Problem: existing DOs
already had data and a __drizzle_migrations table tracking three applied migrations.
We couldn't wipe them. We couldn't re-run the old migrations (the tables already existed).
The migration runner handles this automatically. On first run, it detects the Drizzle
tracking table, counts how many migrations were applied, marks those versions as done
in the new __migrations table, drops the Drizzle table, and continues from where
Drizzle left off. Zero downtime, zero data loss.
Why this works for DOs
Traditional databases have one instance and a migration CLI. DOs have thousands of instances, each with their own SQLite. You can't SSH into them. You can't run a script against them. The only code that touches a DO's database is the DO itself. So the DO must be its own migration runner.
The cost is ~1ms per wake-up to check the tracking table. In practice, after the first
request post-deploy, every subsequent request hits the "nothing to apply" fast path.
The benefit: you never think about migrations again. Add a new mNNNN key, deploy,
and every DO in the world picks it up on next wake.
do-orm on GitHub