1---
2title: Bind to another Worker's Durable Object
3description: Share a Durable Object across multiple Workers — one Worker hosts the runtime, others bind to it by scriptName for a typed RPC stub — and move the host later with the data intact.
4sidebar:
5 order: 9
6---
7
8A Durable Object is _hosted_ by exactly one Worker, but any
9number of **other** Workers can bind to the same DO. This is how
10you share state across Workers: one Worker hosts the DO, every
11other Worker addresses it by `scriptName` and gets a typed RPC
12stub.
13
14By the end of this part you'll have two Workers, `WorkerA` and
15`WorkerB`, sharing a single `Counter` Durable Object. Writes
16through either Worker are visible from the other.
17
18## Move the DO into its own module
19
20The DO class lives in a separate file so both Workers can import
21it as a typed identifier without pulling in the runtime
22implementation.
23
24```typescript
25// src/object.ts
26import type { RuntimeContext } from "alchemy";
27import * as Cloudflare from "alchemy/Cloudflare";
28import * as Effect from "effect/Effect";
29
30export class Counter extends Cloudflare.DurableObject<
31 Counter,
32 {
33 increment: () => Effect.Effect<number, never, RuntimeContext>;
34 get: () => Effect.Effect<number, never, RuntimeContext>;
35 }
36>()("Counter") {}
37```
38
39The class is purely a tagged identifier. Both Workers can import
40it without dragging in the runtime — the bundler tree-shakes
41`.make()` (which we'll add next) out of any consumer that
42doesn't actually host the DO.
43
44## Add the runtime implementation
45
46`Counter.make(...)` provides the per-instance Effect that runs
47inside the DO. Export it as `default` so it can be tree-shaken
48when imported by name.
49
50```diff lang="typescript"
51// src/object.ts
52export class Counter extends Cloudflare.DurableObject<
53 Counter,
54 {
55 increment: () => Effect.Effect<number, never, RuntimeContext>;
56 get: () => Effect.Effect<number, never, RuntimeContext>;
57 }
58>()("Counter") {}
59
60+export default Counter.make(
61+ Effect.gen(function* () {
62+ const state = yield* Cloudflare.DurableObjectState;
63+ return Effect.gen(function* () {
64+ let count = (yield* state.storage.get<number>("count")) ?? 0;
65+
66+ return {
67+ increment: () =>
68+ Effect.gen(function* () {
69+ count += 1;
70+ yield* state.storage.put("count", count);
71+ return count;
72+ }),
73+ get: () => Effect.succeed(count),
74+ };
75+ });
76+ }),
77+);
78```
79
80:::note
81The `state` *reference* is resolved in the outer (Construction) Effect, but
82`state.storage` is
83[colored with `RuntimeContext`](/infrastructure-as-effects/layers#the-types-hold-the-boundary)
84— so the storage reads/writes stay in the inner (runtime) Effect.
85:::
86
87Only Workers that import this file as `import CounterLive from "./object.ts"`
88will pull in the runtime. Workers that only import the class
89(`import { Counter } from "./object.ts"`) get just the type.
90
91## Declare WorkerA as the host
92
93`WorkerA` _hosts_ `Counter` — its bundle contains the DO runtime
94and Cloudflare runs the DO class inside WorkerA's script.
95
96The key move is the **third type argument** on `Cloudflare.Worker`:
97
98```typescript
99// src/workerA.ts
100import * as Cloudflare from "alchemy/Cloudflare";
101import { Counter } from "./object.ts";
102
103export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
104 "WorkerA",
105) {}
106```
107
108`Worker<Self, Bindings, Deps>` — the third slot `Deps` is what
109the script publishes as part of its public contract. By writing
110`, Counter`, we're saying "WorkerA hosts Counter; other scripts
111may bind to it."
112
113:::caution[The third type argument is what unlocks cross-script binding]
114You can omit `, Counter` and the runtime still works — but
115`Counter.from(WorkerA)` from another Worker will be a **TypeScript
116error** because `WorkerA` doesn't declare `Counter` as part of its
117contract. Always declare cross-script DOs in `Deps`.
118:::
119
120## Provide the DO runtime from WorkerA
121
122`WorkerA.make(...)` is where the runtime lives. Provide
123`CounterLive` (the default export from `object.ts`) so that
124WorkerA's bundle actually contains the DO class:
125
126```diff lang="typescript"
127// src/workerA.ts
128import * as Cloudflare from "alchemy/Cloudflare";
129import * as Effect from "effect/Effect";
130+import { HttpServerRequest } from "effect/http/HttpServerRequest";
131+import * as HttpServerResponse from "effect/http/HttpServerResponse";
132-import { Counter } from "./object.ts";
133+import CounterLive, { Counter } from "./object.ts";
134
135export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
136 "WorkerA",
137) {}
138
139+export default WorkerA.make(
140+ { main: import.meta.url },
141+ Effect.gen(function* () {
142+ const counters = yield* Counter;
143+ return {
144+ fetch: Effect.gen(function* () {
145+ const request = yield* HttpServerRequest;
146+ const name = new URL(request.url, "http://x").pathname.slice(1);
147+ const next = yield* counters.getByName(name).increment();
148+ return HttpServerResponse.json({ value: next });
149+ }),
150+ };
151+ }).pipe(Effect.provide(CounterLive)),
152+);
153```
154
155`yield* Counter` inside WorkerA's constructor binds the DO _locally_ —
156WorkerA's `env.Counter` is wired up at deploy time and the class
157ships in the bundle.
158
159## Bind WorkerA's Counter from WorkerB
160
161`WorkerB` does **not** host the DO. It imports `WorkerA` purely
162as a type and uses `Counter.from(WorkerA)` to declare a binding
163to the existing namespace running under WorkerA's script:
164
165```typescript
166// src/workerB.ts
167import * as Cloudflare from "alchemy/Cloudflare";
168import * as Effect from "effect/Effect";
169import { HttpServerRequest } from "effect/http/HttpServerRequest";
170import * as HttpServerResponse from "effect/http/HttpServerResponse";
171import { Counter } from "./object.ts";
172import { WorkerA } from "./workerA.ts";
173
174export default class WorkerB extends Cloudflare.Worker<WorkerB>()(
175 "WorkerB",
176 { main: import.meta.url },
177 Effect.gen(function* () {
178 const counters = yield* Counter.from(WorkerA);
179 return {
180 fetch: Effect.gen(function* () {
181 const request = yield* HttpServerRequest;
182 const name = new URL(request.url, "http://x").pathname.slice(1);
183 const value = yield* counters.getByName(name).get();
184 return HttpServerResponse.json({ value });
185 }),
186 };
187 }),
188) {}
189```
190
191`Counter.from(WorkerA)` reads `WorkerA`'s `Deps` to confirm that
192`Counter` is part of its public contract, then produces a typed
193namespace stub. Under the hood it emits a Cloudflare binding with
194`scriptName: "WorkerA"` so the runtime routes every call to
195WorkerA's hosted instance.
196
197:::tip[Only the host provides `CounterLive`]
198WorkerB imports `Counter` but **never** `CounterLive`. Rolldown
199tree-shakes the runtime out of WorkerB's bundle — only WorkerA
200ships the DO class. This keeps consumer bundles small and avoids
201duplicate runtime instances.
202:::
203
204## Deploy both Workers from one Stack
205
206The Stack composes both Workers. WorkerA's Layer is provided so
207its `Counter` runtime is hooked up; WorkerB consumes it via
208binding only:
209
210```typescript
211// alchemy.run.ts
212import * as Alchemy from "alchemy";
213import * as Cloudflare from "alchemy/Cloudflare";
214import * as Effect from "effect/Effect";
215import WorkerALive, { WorkerA } from "./src/workerA.ts";
216import WorkerB from "./src/workerB.ts";
217
218export default Alchemy.Stack(
219 "CrossWorkerDO",
220 { state: Cloudflare.state(), providers: Cloudflare.providers() },
221 Effect.gen(function* () {
222 const a = yield* WorkerA;
223 const b = yield* WorkerB;
224 return {
225 urlA: a.url.as<string>(),
226 urlB: b.url.as<string>(),
227 };
228 }).pipe(Effect.provide(WorkerALive)),
229);
230```
231
232`Effect.provide(WorkerALive)` is the only place the DO runtime
233enters the program. Without it, `yield* WorkerA` would fail to
234resolve.
235
236## Verify shared state
237
238Hit `urlA/foo` to increment, then `urlB/foo` to read — both
239Workers route to the same DO instance by name:
240
241```typescript
242// test/integ.test.ts
243import * as Cloudflare from "alchemy/Cloudflare";
244import * as Test from "alchemy/Test/Vitest";
245import { expect } from "@effect/vitest";
246import * as Effect from "effect/Effect";
247import * as HttpClient from "effect/http/HttpClient";
248import Stack from "../alchemy.run.ts";
249
250const { test, beforeAll, deploy } = Test.make({
251 providers: Cloudflare.providers(),
252});
253
254const stack = beforeAll(deploy(Stack));
255
256test(
257 "WorkerB reads counter written by WorkerA",
258 Effect.gen(function* () {
259 const { urlA, urlB } = yield* stack;
260 const client = yield* HttpClient.HttpClient;
261
262 yield* client.post(`${urlA}/`);
263 yield* client.post(`${urlA}/`);
264
265 const res = yield* client.get(`${urlB}/`);
266 expect((yield* res.json) as { value: number }).toEqual({ value: 2 });
267 }),
268);
269```
270
271## Prefer `Counter.from(Self)` inside Layers
272
273Inside `WorkerA.make` we wrote `yield* Counter` to bind the
274locally-hosted DO. That works, but it's not the form you want
275when the code might be extracted into a reusable Layer or shared
276between scripts.
277
278`Counter.from(WorkerA)` also works inside `WorkerA` itself —
279calling `.from(Self)` on the host resolves to the same local
280namespace as `yield* Counter`:
281
282```diff lang="typescript"
283// src/workerA.ts
284export default WorkerA.make(
285 { main: import.meta.url },
286 Effect.gen(function* () {
287- const counters = yield* Counter;
288+ const counters = yield* Counter.from(WorkerA);
289 return {
290 fetch: Effect.gen(function* () { ... }),
291 };
292 }).pipe(Effect.provide(CounterLive)),
293);
294```
295
296This matters most when you extract `fetch` logic into a Layer
297(or any service) that wants a `Counter` namespace. With
298`Counter.from(Self)`, the service is explicit about _which_
299script's Counter it talks to. A Worker that hosts its own
300isolated namespace uses `.from(Self)`; a consumer Worker uses
301`.from(Host)`. The shape is identical, which keeps the Layer
302host-agnostic.
303
304:::tip[A Worker can host its own isolated namespace]
305If `WorkerC` declares `Counter` in its own contract
306(`Worker<WorkerC, {}, Counter>`) and provides `CounterLive`,
307then `Counter.from(WorkerC)` from inside `WorkerC` binds to a
308**separate** DO namespace — instances under WorkerC are
309isolated from WorkerA's. This is how you give each script its
310own private set of DO instances using the same class.
311:::
312
313## Move the Durable Object to a new host
314
315Hosting isn't forever — suppose `Counter` should now live under
316`WorkerB` instead of `WorkerA`. A Durable Object class can move
317between Workers **with its data intact**: Cloudflare's
318`transferred_classes` migration re-homes the namespace (same
319instances, same storage) onto the new host's script. Moves are
320always *declared* — a class that disappears from one Worker and
321appears on another is otherwise ambiguous between "transfer the
322data" and "delete it, start fresh" — so the class states where it
323came from.
324
325### Declare where the class came from
326
327Add `transferredFrom` to the class declaration, naming the former
328host:
329
330```diff lang="typescript"
331// src/object.ts
332export class Counter extends Cloudflare.DurableObject<
333 Counter,
334 {
335 increment: () => Effect.Effect<number, never, RuntimeContext>;
336 get: () => Effect.Effect<number, never, RuntimeContext>;
337 }
338->()("Counter") {}
339+>()("Counter", { transferredFrom: "WorkerA" }) {}
340```
341
342`"WorkerA"` is the former host's Worker **logical id**, resolved to
343its physical script through alchemy's ownership tags in the same
344stack + stage (for a cross-stack move, use the physical script name
345instead). Whichever Worker hosts `Counter` next will transfer the
346namespace from it.
347
348:::tip[Prefer a typed reference over a string]
349`transferredFrom` also accepts the Worker class or resource itself —
350or a thunk for forward references and import cycles:
351
352```typescript
353{ transferredFrom: () => WorkerA }
354```
355
356The thunk is evaluated lazily at plan time and normalized to
357WorkerA's logical id, so it stays safe in module cycles (here,
358`object.ts` referencing `workerA.ts` while `workerA.ts` imports
359`object.ts`). A reference never becomes a dependency edge — alchemy
360takes the *identity*, not the deployed value, which keeps the deploy
361order correct (the new host uploads first).
362:::
363
364### Make WorkerB the host
365
366`WorkerB` takes over the host role: it declares `Counter` in its
367`Deps`, provides `CounterLive`, and binds with `.from(Self)`:
368
369```typescript
370// src/workerB.ts
371import * as Cloudflare from "alchemy/Cloudflare";
372import * as Effect from "effect/Effect";
373import { HttpServerRequest } from "effect/http/HttpServerRequest";
374import * as HttpServerResponse from "effect/http/HttpServerResponse";
375import CounterLive, { Counter } from "./object.ts";
376
377export class WorkerB extends Cloudflare.Worker<WorkerB, {}, Counter>()(
378 "WorkerB",
379) {}
380
381export default WorkerB.make(
382 { main: import.meta.url },
383 Effect.gen(function* () {
384 const counters = yield* Counter.from(WorkerB);
385 return {
386 fetch: Effect.gen(function* () {
387 const request = yield* HttpServerRequest;
388 const name = new URL(request.url, "http://x").pathname.slice(1);
389 const value = yield* counters.getByName(name).get();
390 return HttpServerResponse.json({ value });
391 }),
392 };
393 }).pipe(Effect.provide(CounterLive)),
394);
395```
396
397### Turn WorkerA into a consumer
398
399`WorkerA` stops shipping the runtime and binds the same namespace
400cross-script, now hosted by `WorkerB`:
401
402```diff lang="typescript"
403// src/workerA.ts
404-import CounterLive, { Counter } from "./object.ts";
405+import { Counter } from "./object.ts";
406+import { WorkerB } from "./workerB.ts";
407
408-export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
409+export class WorkerA extends Cloudflare.Worker<WorkerA>()(
410 "WorkerA",
411) {}
412
413export default WorkerA.make(
414 { main: import.meta.url },
415 Effect.gen(function* () {
416- const counters = yield* Counter.from(WorkerA);
417+ const counters = yield* Counter.from(WorkerB);
418 return {
419 fetch: Effect.gen(function* () { ... }),
420 };
421- }).pipe(Effect.provide(CounterLive)),
422+ }),
423);
424```
425
426### Provide WorkerB's Layer in the Stack
427
428The Stack now provides both Layers — `WorkerBLive` carries the DO
429runtime, `WorkerALive` no longer does:
430
431```diff lang="typescript"
432// alchemy.run.ts
433import WorkerALive, { WorkerA } from "./src/workerA.ts";
434-import WorkerB from "./src/workerB.ts";
435+import WorkerBLive, { WorkerB } from "./src/workerB.ts";
436
437 Effect.gen(function* () {
438 const a = yield* WorkerA;
439 const b = yield* WorkerB;
440 return {
441 urlA: a.url.as<string>(),
442 urlB: b.url.as<string>(),
443 };
444- }).pipe(Effect.provide(WorkerALive)),
445+ }).pipe(Effect.provide([WorkerALive, WorkerBLive])),
446```
447
448### Deploy the move
449
450```sh
451bun alchemy deploy
452```
453
454`WorkerA` references `WorkerB` through `Counter.from(WorkerB)`, so
455the engine deploys `WorkerB` first. Its upload carries the
456`transferred_classes` migration — the namespace and every stored
457object move to `WorkerB`'s script — and `WorkerA`'s deploy then
458converges on its own (no delete migration is emitted for a class
459that moved away). Every counter keeps its count. Afterwards the
460`transferredFrom` declaration is inert — on fresh stages and on
461every later deploy it does nothing — so leave it in place.
462
463:::caution[Undeclared moves fail loudly]
464If you move the host without declaring `transferredFrom`, the former
465host's deploy fails **before any upload** with
466`DurableObjectTransferRequired`, telling you exactly what to declare.
467Durable Object data is never silently destroyed or forked.
468:::
469
470### Keep the host history for chained moves
471
472If the class later moves again — say `WorkerB` → `WorkerC` — append
473to the declaration rather than replacing it:
474
475```typescript
476// src/object.ts — after a second move
477>()("Counter", { transferredFrom: ["WorkerA", "WorkerB"] }) {}
478```
479
480The namespace transfers from whichever listed host currently holds
481it, so a stage that lagged behind (or skipped the intermediate
482release) still converges from wherever *its* namespace lives.
483
484Two rules for less common shapes:
485
486- **Pure moves** — the former host drops the DO entirely, keeping no
487 cross-script reference — take two deploys: first add the class to
488 the new host with `transferredFrom` and deploy, then remove it from
489 the former host and deploy.
490- **Cross-stack moves** name the former host by physical script name,
491 and the new host's stack deploys first.
492
493## Closing
494
495Two Workers, one DO, type-safe end-to-end. The third type argument
496`, Counter` on `WorkerA` is the small but critical piece that
497makes `Counter.from(WorkerA)` type-check from `WorkerB` — and
498that same form (`Counter.from(Self)`) lets a host script bind to
499its own DO from inside a Layer without caring whether it's the
500host or a consumer.