DurableObjectNamespace
Source:
src/Cloudflare/Workers/DurableObjectNamespace.ts
A Cloudflare Durable Object namespace that manages globally unique, stateful instances with WebSocket hibernation support.
A Durable Object uses a two-phase pattern with two nested Effect.gen
blocks. The outer Effect resolves shared dependencies (other DOs,
containers, etc.). The inner Effect runs once per instance and returns
the object’s public methods and WebSocket handlers.
Effect.gen(function* () { // Phase 1: resolve shared dependencies const db = yield* Cloudflare.D1Connection.bind(MyDB);
return Effect.gen(function* () { // Phase 2: per-instance setup and public API const state = yield* Cloudflare.DurableObjectState;
return { save: (data: string) => db.exec("INSERT ..."), fetch: Effect.gen(function* () { ... }), webSocketMessage: Effect.fnUntraced(function* (ws, msg) { ... }), }; });})There are two ways to define a Durable Object. See the {@link https://alchemy.run/concepts/platform | Platform concept} page for the full explanation.
- Inline — Effect implementation passed directly, single file.
- Modular — class and implementation in separate files for tree-shaking.
Inline Durable Objects
Section titled “Inline Durable Objects”Pass the Effect implementation as the second argument. This is the simplest approach — everything lives in one file. Convenient when the DO doesn’t need to be referenced by other Workers or DOs that would pull in its runtime dependencies.
export default class Counter extends Cloudflare.DurableObjectNamespace<Counter>()( "Counter", Effect.gen(function* () { // init: bind resources const db = yield* Cloudflare.D1Connection.bind(MyDB);
return Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState; const count = (yield* state.storage.get<number>("count")) ?? 0;
return { // runtime: use them increment: () => Effect.gen(function* () { const next = count + 1; yield* state.storage.put("count", next); return next; }), get: () => Effect.succeed(count), }; }); }),) {}Modular Durable Objects
Section titled “Modular Durable Objects”When a Worker and a DO reference each other, or multiple Workers
bind the same DO, define the class separately from its .make()
call. The class is a lightweight identifier; .make() provides
the runtime implementation as an export default. Rolldown treats
.make() as pure, so the bundler tree-shakes it and all its
runtime dependencies out of any consumer’s bundle.
The class and .make() can live in the same file. This is the
same pattern used by Worker and Container.
Modular Durable Object (class + .make() in one file)
export default class Counter extends Cloudflare.DurableObjectNamespace<Counter>()( "Counter",) {}
export default Counter.make( Effect.gen(function* () { // init: bind resources const db = yield* Cloudflare.D1Connection.bind(MyDB);
return Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState; const count = (yield* state.storage.get<number>("count")) ?? 0;
return { // runtime: use them increment: () => Effect.gen(function* () { const next = count + 1; yield* state.storage.put("count", next); yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run(); return next; }), get: () => Effect.succeed(count), }; }); }),);Binding a modular DO from a Worker
// imports Counter; bundler tree-shakes .make()import Counter from "./Counter.ts";
// initconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); return HttpServerResponse.text(String(yield* counter.get())); }),};RPC Methods
Section titled “RPC Methods”Any function you return from the inner Effect becomes an RPC method
that Workers can call through a stub. Methods must return an Effect.
The caller gets a fully typed stub — if your DO returns increment
and get, the stub exposes counter.increment() and counter.get().
return { increment: () => Effect.succeed(++count), get: () => Effect.succeed(count), reset: () => Effect.sync(() => { count = 0; }),};Accessing Instance State
Section titled “Accessing Instance State”Each Durable Object instance has its own transactional key-value
storage via Cloudflare.DurableObjectState. Use storage.get and
storage.put inside the inner Effect to persist data across requests
and restarts.
const state = yield* Cloudflare.DurableObjectState;
yield* state.storage.put("counter", 42);const value = yield* state.storage.get("counter");WebSocket Hibernation
Section titled “WebSocket Hibernation”Durable Objects support WebSocket hibernation — the runtime can
evict the object from memory while keeping connections open. Use
Cloudflare.upgrade() to accept a connection, and return
webSocketMessage / webSocketClose handlers to process events
when the object wakes back up.
Accepting a WebSocket connection
return { fetch: Effect.gen(function* () { const [response, socket] = yield* Cloudflare.upgrade(); socket.serializeAttachment({ id: crypto.randomUUID() }); return response; }),};Handling messages and close events
return { webSocketMessage: Effect.fnUntraced(function* ( socket: Cloudflare.DurableWebSocket, message: string | Uint8Array, ) { const text = typeof message === "string" ? message : new TextDecoder().decode(message); // process the message }), webSocketClose: Effect.fnUntraced(function* ( ws: Cloudflare.DurableWebSocket, code: number, reason: string, ) { yield* ws.close(code, reason); }),};Recovering sessions after hibernation
const state = yield* Cloudflare.DurableObjectState;const sockets = yield* state.getWebSockets();
for (const socket of sockets) { const data = socket.deserializeAttachment<{ id: string }>(); // re-populate your session map}Using from a Worker
Section titled “Using from a Worker”Yield the DO class in your Worker’s init phase to get a namespace
handle. Call getByName or getById to get a typed stub, then
call any RPC method or forward an HTTP request with fetch.
Calling RPC methods
// initconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); yield* counter.increment(); const value = yield* counter.get(); return HttpServerResponse.text(String(value)); }),};Forwarding an HTTP request
// initconst rooms = yield* Room;
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const room = rooms.getByName(roomId); return yield* room.fetch(request); }),};Binding in an Async Worker
Section titled “Binding in an Async Worker”When using an Async Worker (plain async fetch handler, no Effect
runtime), declare Durable Objects in the bindings prop of the
Worker resource. Pass a DurableObjectNamespace reference with a
className matching the exported DurableObject subclass in your
worker source file. Use Cloudflare.InferEnv to get a fully typed
env object that includes the namespace.
Declaring a DO binding in the stack
import type { Counter } from "./src/worker.ts";
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", bindings: { Counter: Cloudflare.DurableObjectNamespace<Counter>("Counter", { className: "Counter", }), },});Using the DO from a plain async handler
import { DurableObject } from "cloudflare:workers";import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { const counter = env.Counter.getByName("my-counter"); const count = await counter.increment(); return new Response(JSON.stringify({ count })); },};
export class Counter extends DurableObject { private counter = 0; async increment() { return ++this.counter; }}