Skip to content

RpcDurableObjectNamespace

Source: src/Cloudflare/Workers/RpcDurableObjectNamespace.ts

RpcDurableObjectNamespace is sugar over {@link DurableObjectNamespace} for Durable Objects whose surface is a typed Effect RpcGroup. The DO serves an RpcServer.toHttpEffect(group) on its own fetch, and consumers see namespace.getByName(id) as a typed RpcClient directly — no manual client wiring.

Use this over alchemy’s built-in DO method bridge whenever values crossing the DO boundary contain Schema.Class instances. The built-in bridge JSON.stringifys every method return value, which strips class identity (e.g. an effect/ai Response.Usage instance becomes a plain struct on the consumer side). With RpcDurableObjectNamespace, both ends go through the same RpcSerialization codec, so Schema.decode reconstructs class instances correctly.

The DO instance is the session, so the group payloads typically don’t include any per-session identifier — only the per-call inputs.

import * as Schema from "effect/Schema";
import { Rpc, RpcGroup } from "effect/unstable/rpc";
const setTitle = Rpc.make("setTitle", {
success: Schema.Void,
payload: { title: Schema.String },
});
const getTitle = Rpc.make("getTitle", {
success: Schema.String,
payload: {},
});
export class CounterRpcs extends RpcGroup.make(setTitle, getTitle) {}

Mirrors Cloudflare.DurableObjectNamespace<Self>()(...) — same outer/inner Effect pattern. The outer Effect resolves shared deps; the per-instance inner Effect returns the RpcServer.toHttpEffect(schema)-piped Effect directly.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
import { CounterRpcs } from "./rpcs.ts";
export default class Counter extends Cloudflare.RpcDurableObjectNamespace<Counter>()(
"Counter",
{ schema: CounterRpcs },
Effect.gen(function* () {
// outer init: shared deps for all instances
return Effect.gen(function* () {
// per-instance init: state + handlers
const state = yield* Cloudflare.DurableObjectState;
const handlers = CounterRpcs.toLayer({
setTitle: ({ title }) => state.storage.put("title", title),
getTitle: () =>
Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
});
return RpcServer.toHttpEffect(CounterRpcs).pipe(
Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)),
);
});
}),
) {}

yield* Counter resolves to a value whose getByName(id) returns a typed RpcClient<CounterRpcs>. Each rpc method is a typed Effect/Stream factory — no RpcClient.make setup needed.

import Counter from "./counter.ts";
Effect.gen(function* () {
const counters = yield* Counter;
yield* counters.getByName("global").setTitle({ title: "Hello" });
const title = yield* counters.getByName("global").getTitle({});
return title;
});

Yielding the surrounding namespace from inside a DO

Section titled “Yielding the surrounding namespace from inside a DO”

Lets a DO instance refer to its own namespace — e.g. to fan a call out to sibling instances. Mirrors yield* DurableObjectNamespace on the regular DurableObjectNamespace.

Effect.gen(function* () {
const self = yield* Cloudflare.RpcDurableObjectNamespace;
yield* self.getByName("peer-1").setTitle({ title: "Sibling call" });
});