Skip to content

Durable Objects

Tamer manages Durable Object class bindings and class migrations (wrangler migrations[]). It does not provision or track individual instances — apps create those at runtime with getByName / idFromName.

SQLite schema inside a DO (tables, Drizzle durable-sqlite, etc.) is app-owned. That is not D1 and is not tamer migrate.

Config

ts
workers: {
  api: {
    path: "./workers/api",
    main: "src/index.ts",
    resources: {
      durableObjects: [
        {
          logicalName: "chat-room",
          className: "ChatRoom",
          binding: "CHAT_ROOM", // optional; default DO_CHAT_ROOM
          // sqlite: true (default) — create via new_sqlite_classes
          // scriptName?: "other-worker-${tamer:env}" — cross-script host
        },
      ],
    },
    // Append-only class history — never auto-derived from durableObjects[]
    doMigrations: [
      { tag: "v1", new_sqlite_classes: ["ChatRoom"] },
    ],
  },
}

WFP tenant scripts use the same pair on the worker template:

ts
wfp.namespaces.portal.workers.app: {
  main: "src/index.ts",
  durableObjects: [{ logicalName: "chat-room", className: "ChatRoom" }],
  doMigrations: [{ tag: "v1", new_sqlite_classes: ["ChatRoom"] }],
}

When migrations run

CommandBehavior
tamer deploy / wfp tenant provisionEmit durable_objects + migrations; wrangler applies pending tags
wfp tenant reset --kind d1 / --target … / wfp tenant add-shardRebinding onlymigrations[] is trimmed to the tags Cloudflare already applied, so DO instances are never touched and --confirm-do-delete is not needed
wfp tenant reset --kind durable_objectWipes DO instances + data (see Reset)
tamer migrateD1 only — never DO class migrations

The applied head comes from the script's live migration_tag, falling back to the tags Tamer recorded at the last upload. Tamer fails closed — sends the full migrations[] and applies the --confirm-do-delete gate — when freezing would be wrong:

  • the applied head is unknown (script missing on Cloudflare, or a live tag the config's doMigrations does not contain, e.g. after pruning old tags), or
  • a class in durableObjects[] is only created by an unapplied tag. Bindings come from durableObjects[], not from migrations[], so the create tag has to ship with the binding. (Classes no migration declares — brownfield adopt — don't trigger this.)

Rename

Preserve instances:

  1. Append { tag: "v2", renamed_classes: [{ from: "ChatRoom", to: "ChatRoomV2" }] } to doMigrations.
  2. Update durableObjects[].className and the Worker export to ChatRoomV2.
  3. Deploy once (avoid gradual deploy for this change).

Delete (data wipe)

  1. Remove the binding from durableObjects and all code references.
  2. Append { tag: "vN", deleted_classes: ["ChatRoom"] }.
  3. Deploy / provision with --confirm-do-delete. Tamer blocks new deleted_classes tags until that flag is set (subsequent deploys of the same already-applied tag do not need it again).

The gate only fires when there is something to wipe: a script Cloudflare definitively reports as missing (fresh provision) replays the full history, deleted_classes included, without the flag, since a nonexistent script has no DO instances. If the live lookup fails outright, Tamer cannot prove that and fails closed — the error names the lookup failure.

Envs matching wfp.ephemeralEnvPattern never gate on tenant provisions, even when the script already exists (a reused PR env whose script predates the delete tag). Ephemeral env data is disposable by declaration — env-gc destroys the whole script, DO instances included, without confirmation — so Tamer auto-confirms and logs the decision, keeping CI provisions unattended.

Whether a tag counts as applied is decided from Cloudflare's live migration_tag first — everything up to and including it, since tags apply in list order — unioned with the tags Tamer recorded. Live truth first means a state gap cannot re-demand the flag for a wipe that already happened.

Reset (tenant scripts)

Cloudflare applies each migration tag once per script and offers no "re-run this tag" operation, so re-deploying an already-applied deleted_classes tag wipes nothing. To actually clear a tenant's DO instances + stored data:

bash
tamer wfp tenant reset --env dev --workspace acme --kind durable_object --confirm-do-delete

That deletes the tenant dispatch script (Cloudflare drops the script's DO namespaces with it), uploads it again so doMigrations replay from the first tag, and re-pushes the template's vault secrets (dispatch secrets live on the script and go away with it). It touches no D1 database and runs no migrate/seed. Protected envs still require --confirm-tenant / --force.

Scope notes:

  • --kind durable_object is script-wide; --target durable_object:<logical> is rejected. To drop one class, append a deleted_classes tag (below).
  • --service <name> narrows to one worker template.
  • Not supported for --env local (miniflare DO state lives in the worker's .wrangler/state directory — delete that).

Brownfield adopt

  • Do not emit a fresh new_sqlite_classes for a class already live on the script — author doMigrations so tags match what Cloudflare has already applied (live migration_tag is readable via the Workers script API).
  • tamer import --target durable_object:<logical> records binding intent from config; it does not invent migration history.
  • tamer drift compares config head tag to live migration_tag when the API allows, and flags pending deleted_classes tags not yet recorded as applied.

Facets / Dynamic Workers

Tamer does not inventory Durable Object facets. Pattern:

  1. Declare a supervisor DO class with managed durableObjects + doMigrations (this guide).
  2. Pass through Wrangler worker_loaders (or equivalent) on the same worker for Dynamic Worker loading — still passthrough today.
  3. Create/delete facets at runtime in app code. Facet census is not a Tamer apply target.

Passthrough

Do not mix managed resources.durableObjects / doMigrations with raw wrangler durable_objects / migrations on the same worker — Tamer rejects the conflict. Raw passthrough remains valid when you are not using the managed fields. tamer deploy topo-sorts on both passthrough and managed scriptName / script_name so DO hosts deploy before dependents.

Cross-script bindings and env isolation

DO namespaces belong to the script, and every deployed script name is env-suffixed (stack workers and WFP tenant scripts alike — {service}-{workspace}-{env}) — so same-script DO bindings (no scriptName) are isolated per env automatically, with no env token needed on the class or binding name.

scriptName is written to wrangler verbatim except for ${tamer:env} substitution. A cross-script binding should carry the token (scriptName: "api-service-${tamer:env}"); a bare literal points every env at the same host script, sharing its DO instances across environments. The class belongs to the host worker's doMigrations — do not redeclare it in the binding worker's history (cross-script bindings are exempt from the local class-presence validation).

Released under the Tamer Evaluation License.