---
title: Durable Object transfer
description: Namespace ownership, callbacks, alarms and rollback for the move of the Durable Objects to the API Worker.
---

:::warning[Owner-run, by hand]
This applies only to an instance whose Durable Objects still live in the storefront Worker. There is no scripted
rehearsal: an owner who transfers the namespaces does it by hand on a disposable stage first and records what they
observe. This page records no transfer or production observation.
:::

The stable logical IDs are `ShopDatabase`, `ConversationHub`, `AccountWriter`, `Shop`, and `Api`. The API declares
both classes with `transferredFrom: ['Shop']`. Keep those declarations indefinitely: they cover lagging stages and
are inert after transfer. The storefront binds both classes from the API through `scriptName: api.worker.workerName`
and never declares them. The API is uploaded first because the storefront depends on its service binding.

The hub's callback paths are served by the API Worker:

- `POST /internal/conversations/{id}/turn`
- `POST /internal/conversations/{id}/handoff-timeout`

They use `ctx.exports.default` on the class's current host, authenticated by `Authorization: Bearer
<CONVERSATION_HUB_SECRET>`. Preserve the existing `ConversationHubSecret` value across the move. The same release must
contain the API callbacks and Fern.

## Disposable stage

Use a stage named `rehearsal-YYYYMMDD`, dev-tenant identities and separate workers.dev origins. Never attach production
custom domains. Leave `ANTHROPIC_API_KEY` unset on the stage API so the read-only fallback answers conversations.
Use `search tent`, `cart`, `orders`, `order <number or ID>` or `help`; other messages show its limitations.
Verify that `add two tents to my cart` changes nothing and explains that no human handoff is available.
Start deletion of a test account before the transfer, so AccountWriter has persisted its `blocked` state, and check after
the move that the blocked writer still refuses. Separately verify a conversation turn and the ConversationHub
alarm on an open conversation. AccountWriter has no alarm; account deletion resumes through
[explicit recovery](/runbooks/account-recovery).

## Rollback

A pre-transfer application rollback cannot undo namespace ownership. Keep an owner-created rollback build containing the
earlier application behavior **and** the transferred topology: API class exports and callbacks, API
`transferredFrom` declarations, Shop cross-script bindings, no Shop class exports. Run `pnpm ready` on that build.

Deploy it to the disposable stage and verify again. Then try a separate emergency reverse transfer: Shop declares
`transferredFrom: ['Api']`, API references Shop, and both callbacks are hosted with the classes. Verify, transfer back
to the API, and verify again. Reverse transfer is never the first incident response. Destroy only the disposable stage
after recording the results. No production move is allowed until the owner has recorded all of these observations.

## Owner smoke after the transfer

Use a disposable test account and the registered agent credentials described in `scripts/smoke/poppy-direct.ts`. Run it
with `POPPY_SMOKE_CONVERSATION=1` to add a Fern conversation round trip and an open event stream. After the agent is
disconnected in the browser, the stream must end without an `event: error`, and a fresh read must be refused with a
challenge. The script also prompts the operator to change, reload and restore a disposable address on
`/account/addresses`. Retain that browser observation with the release record.

Check that the first hourly invocation after a release completes all five API jobs by reading the Worker's JSON tail for
`scheduled_job` records with `worker: "api"` and `outcome: "ran"`. Successful jobs delete their lease rows, so an empty
`job_leases` table after completion is expected.
