# How the Capnweb RPC Protocol Connects Durable Objects to computerd

> Understand how the Capnweb RPC protocol connects Durable Objects to computerd via WebSockets. Learn about its SyncRPC and ShellRPC interfaces for efficient data exchange.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-09-04

---

**The capnweb RPC protocol operates over a single long-lived WebSocket on `/api`, using text-frame JSON to expose a composite `WorkspaceRPC` interface that splits into `SyncRPC` for filesystem operations and `ShellRPC` for command execution.**

The capnweb RPC protocol serves as the exclusive communication channel between Cloudflare Durable Objects (DO) and the sandboxed `computerd` process in the cloudflare/computer repository. This lightweight, text-based protocol enables real-time filesystem synchronization and remote shell execution across a single WebSocket transport, with automatic reconnection handling on the client side.

## WebSocket Transport and Session Lifecycle

The protocol relies on a single HTTP GET request to `/api` that upgrades into a long-lived WebSocket connection. In [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts), the Durable Object creates the RPC stub **before** the WebSocket handshake completes, queuing any calls until the socket is ready. Once established, the connection persists for the entire workspace lifetime.

Binary frames are explicitly unsupported; all messages use **text-frame JSON** format. If the socket closes or errors, the DO synchronously discards the stub. Subsequent RPC calls automatically trigger a fresh connection attempt, ensuring transparent re-establishment of the session without client intervention.

## The WorkspaceRPC Interface Architecture

At the core of the protocol is the `WorkspaceRPC` interface defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts). This root stub merges two independent APIs under one surface:

```typescript
export interface WorkspaceRPC {
  sync:  SyncRPC;
  shell: ShellRPC;
}

```

The DO accesses these halves via `rpc.sync` and `rpc.shell`, allowing a single connection to handle both filesystem state and process execution. The server implementation in [`packages/rpc/src/server.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/server.ts) constructs this composite stub and wires it to the WebSocket endpoint.

## Filesystem Synchronization via SyncRPC

The `SyncRPC` interface handles all virtual filesystem operations between the DO and `computerd`.

### Pushing Changes with Revision Tracking

The `push` method streams a batch of `ChangeEntry` records containing paths and hashes. A critical parameter is `senderRev`:

- **`senderRev > 0`**: Indicates the push originates from a sync peer
- **`senderRev === 0`**: Indicates the push comes from an external orchestrator

The container replies with its new revision and a cursor marking the applied batch, allowing precise synchronization tracking.

### Streaming Remote Changes

The `fetchChanges` method accepts a cursor—structured as `{ rev: number, path: string | null }`—and returns a `ReadableStream<ChangeEntry>`. When `path` is `null`, the stream includes all changes up to the specified revision. This enables efficient incremental sync without polling.

### Object Management and Diagnostics

Additional methods provide visibility into container state:

- **`watermarks`**: Returns current revision markers
- **`hasObjects`**, **`fetchObjects`**, **`pushObjects`**: Efficient bulk transfer of file contents
- **`readEntry`**: Direct metadata inspection

These primitives allow the DO to maintain consistency while minimizing data transfer.

## Remote Execution via ShellRPC

The `ShellRPC` interface provides sandboxed command execution inside the `computerd` container.

### Starting Processes

The `exec` method accepts a command or module source and returns an execution handle. The handle includes an `events` field—a `ReadableStream<ExecEvent>`—that delivers stdout, stderr, and exit frames in real time.

### Process Lifecycle Management

The DO manages running processes through three key methods:

- **`getExec`**: Re-attaches to an existing execution by ID
- **`killExec`**: Signals a running process to terminate
- **`disposeExec`**: Cleans up resources and closes the execution context

This design supports long-running tasks that may outlive individual WebSocket connections, with the ability to reconnect and resume monitoring.

## Client Implementation and Reconnection

The `createWorkspaceClient` helper in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) manages connection state. It buffers RPC calls made before the WebSocket opens, flushing them immediately upon connection. When the socket closes, the client discards the underlying stub; the next method invocation automatically creates a fresh session, ensuring resilience against network interruptions.

## Protocol Versioning Constraints

The capnweb protocol currently has **no built-in version negotiation**. Any change to request or response shapes constitutes a hard breaking change, requiring lock-step deployment of both DO and `computerd` components. This design trades flexibility for simplicity, assuming both endpoints run compatible code versions.

## Practical Implementation Example

The following pattern from [`packages/computer/src/backends/container/cloudflare-container.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/cloudflare-container.ts) demonstrates typical usage:

```typescript
import { createWorkspaceClient } from "@cloudflare/computer-rpc";

// Create client stub (queues calls until WebSocket ready)
const rpc = await createWorkspaceClient("ws://localhost:45678/api");

// Push filesystem changes as a sync peer
await rpc.sync.push({
  senderRev: 42,
  changes: new ReadableStream({
    start(controller) {
      controller.enqueue({ 
        path: "/src/main.ts", 
        hash: new Uint8Array([/* ... */]) 
      });
      controller.close();
    },
  }),
});

// Fetch remote changes since revision 42
const { stream } = await rpc.sync.fetchChanges({
  after: { rev: 42, path: null },
});

// Execute shell command and stream output
const { id, events } = await rpc.shell.exec({
  source: "ls -l /workspace",
});

for await (const ev of events) {
  if (ev.name === "stdout") {
    console.log(new TextDecoder().decode(ev.value));
  }
  if (ev.name === "exit") {
    console.log(`Process exited with code ${ev.code}`);
  }
}

```

## Summary

- **Transport**: Single WebSocket on `/api` using text-frame JSON only; binary frames are unsupported.
- **Interface**: `WorkspaceRPC` combines `SyncRPC` and `ShellRPC` under one root stub defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts).
- **Sync**: Push changes with `senderRev` semantics, fetch incremental updates via cursors, and transfer file objects efficiently.
- **Execution**: Spawn processes with `exec`, managing lifecycle through `getExec`, `killExec`, and `disposeExec` with streaming event output.
- **Resilience**: Client-side stub recreation and call buffering ensure transparent reconnection without application-level handling.
- **Versioning**: Hard protocol compatibility requires synchronized deployment; no runtime negotiation exists.

## Frequently Asked Questions

### What transport mechanism does the capnweb RPC protocol use?

The protocol uses a single long-lived WebSocket connection established via an HTTP GET upgrade to `/api`. All communication occurs over text frames containing JSON payloads; binary WebSocket frames are explicitly unsupported by the current implementation.

### How does the Durable Object handle connection failures?

The DO immediately discards the RPC stub when the WebSocket closes or errors. The client implementation in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) automatically creates a fresh stub on the next method call, buffering any queued requests until the new connection establishes, ensuring seamless reconnection without manual intervention.

### What is the difference between SyncRPC and ShellRPC?

**SyncRPC** manages filesystem state through methods like `push`, `fetchChanges`, and `watermarks`, handling revision-tracked file synchronization. **ShellRPC** provides process execution capabilities via `exec`, `killExec`, and related methods, returning streaming output and exit codes from commands running inside the `computerd` container.

### Why does the protocol lack version negotiation?

The capnweb RPC protocol omits versioning to minimize complexity and overhead. Because the DO and `computerd` are typically deployed together as a unified system, the implementation assumes both endpoints run compatible code versions. Any schema changes require coordinated, lock-step rollouts to prevent deserialization errors.