Skip to content

RPC server and typesafe method exposure

createRpcServer turns a Durable Object into a WebSocket RPC endpoint. It exposes three responsibilities:

  • Accept WebSocket upgrades and attach hibernation-safe subscription metadata.
  • Dispatch RPC requests to methods on the DO instance and respond with results.
  • Emit events to subscribed sockets when a method succeeds.

When fetch() receives an upgrade request:

  • A WebSocketPair is created.
  • The server socket is accepted via ctx.acceptWebSocket.
  • A socket attachment is initialized with { subscriptions: [] }.

This attachment is the source of truth for which methods each socket subscribes to.

createRpcServer understands two payload shapes:

  • RPC requests: { id, method, args }.
  • Subscription requests: { id, type: 'subscribe' | 'unsubscribe', method }.

Subscription requests mutate the socket attachment by adding/removing method names. Events only go to sockets whose attachment includes the method (or '*').

When a request arrives:

  1. The payload is parsed and validated.
  2. The method is looked up in the methods map.
  3. The method is invoked and the result is returned in a response.
  4. An event is emitted to all other subscribers of that method.

This gives you consistent outcomes: the caller gets the response, and all listeners get the same result as a broadcast event.

Events only broadcast when the mutation runs through the RPC server. If you update storage directly in the DO without going through the RPC flow, no event is emitted.

Advanced: server-initiated events with callAndEmit

Section titled “Advanced: server-initiated events with callAndEmit”

Sometimes you want to emit an event from inside the DO without a client WebSocket request (for example, a scheduled task or a DO-to-DO call). The inventory DO exposes a typesafe helper that enforces allowed methods and argument types:

async callAndEmit<M extends EmitMethodName>({
method,
args,
}: {
method: M;
args: EmitArgs<M>;
}): Promise<EmitReturn<M>> {
if (!inventoryRpcMethods.includes(method)) {
throw new Error(`Method ${method} is not allowed for callAndEmit.`);
}
return this.rpcServer.callAndEmit(method, args) as EmitReturn<M>;
}

This keeps server-initiated events typesafe and uses the same event payload that a WebSocket caller would trigger, so subscribers stay in sync.

In the inventory DO, the server keeps RPC types in sync by:

  • Defining a list of method names (inventoryRpcMethods).
  • Deriving InventoryRpc and CurrencyRpc from the DO class itself via RpcFromTarget.
  • Using Pick to restrict the exposed surface to the allowed methods.
const inventoryRpcMethods = [
'addEntry',
'updateEntry',
'transferEntry',
'transferEntries',
'reorderEntries',
'removeEntry',
'removeEntries',
'updateCurrency',
'transferCurrency',
'getEntry',
'getCurrency',
] as const;
export type InventoryRpc = Pick<
RpcFromTarget<InstanceType<typeof InventoryDO>, 'fetch'>,
(typeof inventoryRpcMethods)[number]
>;

This pattern prevents drift: if a method signature changes on the DO class, the client-facing RPC type changes with it.

this.rpcServer = createRpcServer(
this as unknown as DurableObjectLike,
inventoryRpcMethods
);

And then forward the WebSocket hooks:

async fetch(request: Request) {
return this.rpcServer.fetch(request);
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
await this.rpcServer.handleMessage(ws, message);
}

The DO implementation stays focused on domain logic, while the server handles transport and subscription routing.

createRpcServer also accepts resultTransforms, so server output (result in responses/events) can be serialized in one place and deserialized by the client with the same profile.

const rpcTransforms = createRpcTransforms<InventoryRpc>()
.method('transferCurrency', {
name: 'better-result',
serialize: serializeBetterResult,
deserialize: deserializeBetterResult,
})
.done();
this.rpcServer = createRpcServer(
this as unknown as DurableObjectLike,
inventoryRpcMethods,
{
resultTransforms: {
response: rpcTransforms,
event: rpcTransforms,
},
}
);