Durable Object Storage
Keep a tenant's migration history in its Durable Object's own storage
Quick Start
The history lives next to the object that changes it: reads are local, and the object is the only writer.
import { DurableObject } from "cloudflare:workers";
import { createMigrationClient } from "viborm/migrations";
import { createDurableObjectStorageWriter } from "viborm/migrations/storage/durable-object";
export class Tenant extends DurableObject<Env> {
// this.client: the tenant's VibORM client
async generate(name: string) {
const migrations = createMigrationClient(this.client, {
storage: createDurableObjectStorageWriter(this.ctx.storage),
});
return migrations.generate({ name });
}
}
The class must be SQLite-backed. Declare it under new_sqlite_classes in its
first migration:
// wrangler.jsonc
{
"durable_objects": {
"bindings": [{ "name": "TENANT", "class_name": "Tenant" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Tenant"] }]
}
The entry loads without nodejs_compat. The database driver you pair it with
may still need it: pg and postgres.js do.
Configuration
const storage = createDurableObjectStorageWriter(ctx.storage, {
prefix: "h2/",
});
| Parameter | Type | Description |
|---|---|---|
storage |
DurableObjectStorage |
ctx.storage of a SQLite-backed Durable Object |
options.prefix |
string |
Optional key prefix: one history per prefix, so an object can hold a pruned history beside its current one. Default "". |
How Publication Stays Atomic
Every artifact is one row of the viborm_migration_estate table, created on
first use. A publish is one statement:
INSERT INTO viborm_migration_estate (key, bytes) VALUES (?, ?)
ON CONFLICT (key) DO NOTHING RETURNING key
The SQL API is synchronous, so the existence check and the write are one SQLite statement with nothing awaited between them. Concurrent requests to the object, or concurrent publishes from one request, cannot interleave inside it: exactly one creates the key, and every other one reads the stored bytes back and is refused as corruption if they differ.
Limits
- One estate per prefix. Use one object per tenant, or an R2 bucket with a prefix per estate.
- 2 MB per artifact. A snapshot, SQL blob or manifest is one row, so it is bounded by the Durable Object row size.
- Leave the table alone. Rows are content-addressed and immutable; writing or deleting them yourself corrupts the history.
- Restores. The history lives in the object and the migration marker in
the database, so restore both to matching points. A history restored behind
the database’s marker makes
applyrefuse withMIGRATION_NOT_FOUND(the marker is ahead of, or unknown to, this history); a history ahead of the marker shows its later states as pending migrations.