# Capnweb RPC Wire Types: Differences Between SyncRPC and ShellRPC and How They Share Transport

> Explore Capnweb RPC wire types: SyncRPC vs ShellRPC. Discover how they share transport over a single WebSocket session for efficient filesystem sync and process execution.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: deep-dive
- Published: 2026-08-14

---

**Capnweb uses two distinct RPC wire types—SyncRPC and ShellRPC—that share a single WebSocket session through a unified `WorkspaceRPC` stub, eliminating the need for separate version negotiation between filesystem synchronization and process execution channels.**

Capnweb is the RPC framing layer that connects a **Cloudflare Durable Object (DO)** to the in-container `computerd` process. Its wire protocol transmits **single JSON-text frames** over a persistent WebSocket, with an HTTP-batch fallback for reliability. Understanding how the two wire types differ—and how they coexist on the same transport—is essential for anyone building or debugging on the Cloudflare Computer platform.

## The Two Capnweb RPC Wire Types

Capnweb splits its wire surface into **two logical halves**, each serving fundamentally different purposes.

### SyncRPC: Filesystem and Blob Synchronization

**SyncRPC** handles bidirectional data flow between the DO and container, focusing on state reconciliation.

- **Direction:** DO → container (push) and container → DO (fetch)
- **Primary purpose:** Synchronizes filesystem changes, object blobs, and watermarks
- **Key operations:**
  - `push()` — streams `ChangeEntry` records to the remote
  - `fetchChanges()` — pulls changes after a specified cursor
  - `hasObjects()` — probes for missing blobs by hash
  - `pushObjects()` / `fetchObjects()` — transfers actual byte content

This interface is defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) as the `SyncRPC` interface and exposed via `WorkspaceRPC.sync`.

### ShellRPC: Process Execution and Supervision

**ShellRPC** manages containerized process lifecycle and event streaming.

- **Direction:** Bidirectional (DO ↔ container)
- **Primary purpose:** Spawns commands, manages execution state, and streams output
- **Key operations:**
  - `exec()` — spawns a new command with `source` and returns execution ID plus `ExecEvent` stream
  - `getExec()` — re-attaches to running or completed executions
  - `killExec()` — sends signals to running processes
  - `disposeExec()` — cleans up execution logs and resources

All exec events stream as `ExecEvent` records. This interface is declared alongside SyncRPC in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) and reached via `WorkspaceRPC.shell`.

## How the Wire Types Share Transport

Both halves share the **same WebSocket session** and the **same Capnweb stub**. This design decision has important consequences for versioning and contract evolution.

### Unified Stub Architecture

The DO creates a single stub for `WorkspaceRPC`. That stub contains two sub-stubs:

```typescript
// Conceptual structure of WorkspaceRPC
interface WorkspaceRPC {
  sync: SyncRPC;   // filesystem synchronization
  shell: ShellRPC; // process execution
}

```

Because the contract is defined in **one TypeScript interface**, changes to either half do **not** require separate version negotiation. The entire contract evolves lock-step. This keeps the wire surface stable while allowing independent testing of each logical group.

### Wire Serialization Format

The actual JSON messages are generated by the Capnweb library. Each RPC call serializes to:

- Method name
- Arguments
- Unique request ID

Responses carry either the result or a structured `WireError`. Error codes are defined in the same interface (`WireErrorCode`) and propagate unchanged across the wire. Callers can branch on `err.code` directly without parsing message text.

## Working with Both Wire Types in Practice

Here is how to create a client that exercises both SyncRPC and ShellRPC through the shared transport:

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

// Connect to the DO-side WebSocket (URL provided by host)
const client: WorkspaceRPC = await createWorkspaceClient('ws://my-do.example.com/ws');

// ----- SyncRPC: filesystem synchronization -----
await client.sync.push({
  senderRev: 42,
  changes: changeStream,  // ReadableStream<ChangeEntry>
});

const { currentCursor, stream } = await client.sync.fetchChanges({
  after: { rev: 0, path: null },
});

// ----- ShellRPC: process execution -----
const { id, events } = await client.shell.exec({
  source: 'node -e "console.log(\'hello\')"',
});

for await (const ev of events) {
  if (ev.name === 'stdout') {
    console.log(new TextDecoder().decode(ev.value));
  }
}

```

### Handling Wire-Level Errors

The shared error schema enables portable error handling across both RPC types:

```typescript
try {
  await client.sync.hasObjects([hash1, hash2]);
} catch (e) {
  if (e.code === 'EUNKNOWN_HASH') {
    // Remote does not recognize one or more hashes — probe or retry
  }
}

```

## Key Source Files

| File | Role |
|------|------|
| [`docs/08_capnweb_interface.md`](https://github.com/cloudflare/computer/blob/main/docs/08_capnweb_interface.md) | Transport, framing, and overall RPC contract description |
| [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) | Concrete `SyncRPC`, `ShellRPC`, and `WorkspaceRPC` TypeScript interfaces, including `WireErrorCode` definitions |
| [`packages/dofs/src/sync/changes.ts`](https://github.com/cloudflare/computer/blob/main/packages/dofs/src/sync/changes.ts) | `ChangeEntry` schema referenced by sync RPC |
| [`packages/computerd/src/exec/log.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/log.ts) | Exec event storage that Shell RPC streams |
| [`packages/rpc/README.md`](https://github.com/cloudflare/computer/blob/main/packages/rpc/README.md) | High-level RPC package overview and usage examples |

## Summary

- **SyncRPC** and **ShellRPC** are the two Capnweb RPC wire types, handling filesystem synchronization and process execution respectively
- Both share a **single WebSocket session** through the unified `WorkspaceRPC` stub
- The **lock-step contract evolution** eliminates version negotiation complexity
- **Structured wire errors** with stable codes enable reliable cross-network error handling
- All definitions live in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) with architectural documentation in [`docs/08_capnweb_interface.md`](https://github.com/cloudflare/computer/blob/main/docs/08_capnweb_interface.md)

## Frequently Asked Questions

### What distinguishes SyncRPC from ShellRPC in Capnweb?

SyncRPC synchronizes filesystem state, object blobs, and watermarks between the DO and container, using operations like `push()`, `fetchChanges()`, and `hasObjects()`. ShellRPC manages process lifecycle—spawning commands with `exec()`, re-attaching with `getExec()`, and streaming output via `ExecEvent` records. They operate on entirely different domains but share the same transport mechanism.

### Why do both wire types use the same stub instead of separate connections?

Sharing a single `WorkspaceRPC` stub over one WebSocket simplifies connection management and enforces **lock-step contract evolution**. Because both halves are declared in one TypeScript interface, the system avoids version negotiation overhead. The trade-off is that changes to either half require coordinated deployment, but this is acceptable for the tightly coupled DO-container relationship.

### How are errors handled consistently across SyncRPC and ShellRPC?

Both wire types use the same `WireError` structure with codes defined in `WireErrorCode`. When the Capnweb library serializes a response, errors propagate with their code intact. This lets callers switch on `err.code`—such as `'EUNKNOWN_HASH'` for missing blobs—without parsing opaque message strings, enabling portable error handling logic.

### Where is the Capnweb RPC contract formally defined?

The concrete TypeScript interfaces are in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts), which exports `SyncRPC`, `ShellRPC`, `WorkspaceRPC`, and `WireErrorCode`. The architectural design, transport semantics, and framing details are documented in [`docs/08_capnweb_interface.md`](https://github.com/cloudflare/computer/blob/main/docs/08_capnweb_interface.md) at the repository root.