Skip to content
VibORM
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 apply refuse with MIGRATION_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.

Next Steps

Was this page helpful?