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
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:
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
| Command | Behavior |
|---|---|
tamer deploy / wfp tenant provision | Emit durable_objects + migrations; wrangler applies pending tags |
wfp tenant reset --kind d1 / --target … / wfp tenant add-shard | Rebinding only — migrations[] 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_object | Wipes DO instances + data (see Reset) |
tamer migrate | D1 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
doMigrationsdoes not contain, e.g. after pruning old tags), or - a class in
durableObjects[]is only created by an unapplied tag. Bindings come fromdurableObjects[], not frommigrations[], so the create tag has to ship with the binding. (Classes no migration declares — brownfield adopt — don't trigger this.)
Rename
Preserve instances:
- Append
{ tag: "v2", renamed_classes: [{ from: "ChatRoom", to: "ChatRoomV2" }] }todoMigrations. - Update
durableObjects[].classNameand the Worker export toChatRoomV2. - Deploy once (avoid gradual deploy for this change).
Delete (data wipe)
- Remove the binding from
durableObjectsand all code references. - Append
{ tag: "vN", deleted_classes: ["ChatRoom"] }. - Deploy / provision with
--confirm-do-delete. Tamer blocks newdeleted_classestags 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:
tamer wfp tenant reset --env dev --workspace acme --kind durable_object --confirm-do-deleteThat 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_objectis script-wide;--target durable_object:<logical>is rejected. To drop one class, append adeleted_classestag (below).--service <name>narrows to one worker template.- Not supported for
--env local(miniflare DO state lives in the worker's.wrangler/statedirectory — delete that).
Brownfield adopt
- Do not emit a fresh
new_sqlite_classesfor a class already live on the script — authordoMigrationsso tags match what Cloudflare has already applied (livemigration_tagis readable via the Workers script API). tamer import --target durable_object:<logical>records binding intent from config; it does not invent migration history.tamer driftcompares config head tag to livemigration_tagwhen the API allows, and flags pendingdeleted_classestags not yet recorded as applied.
Facets / Dynamic Workers
Tamer does not inventory Durable Object facets. Pattern:
- Declare a supervisor DO class with managed
durableObjects+doMigrations(this guide). - Pass through Wrangler
worker_loaders(or equivalent) on the same worker for Dynamic Worker loading — still passthrough today. - 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).