What Is Cap'n Web RPC and How Cloudflare OS Uses It for Secure Edge Communication
Cap'n Web RPC is Cloudflare's lightweight, bidirectional object-capability protocol that enables type-safe remote procedure calls over WebSockets between browser sandboxes and edge Durable Objects.
The cloudflare/cloudflare-os repository (also known as the Gadgets Workshop platform) uses Cap'n Web as its default RPC mechanism to connect frontend agents, backend gatekeepers, and sandboxed gadget code. This protocol extends the Cap'n Proto design philosophy to run efficiently over WebSocket connections or Cloudflare Workers' built-in RPC channels, operating seamlessly in both browser iframes and edge environments.
Core Architecture of Cap'n Web RPC
Cap'n Web implements an object-capability model where remote objects appear as local JavaScript stubs. The protocol serializes method calls into messages that traverse WebSocket connections, allowing bidirectional communication between untrusted browser code and privileged server-side resources.
Bidirectional Stubs and Object Capabilities
The fundamental abstraction in Cap'n Web is the RpcTarget. When a client connects, RpcSession exports a target object that appears as a normal JavaScript object on the remote side. Method invocations on these stubs automatically serialize into RPC messages, while the server returns stubs or values that can be invoked immediately.
According to the AGENTS documentation, the protocol establishes persistent WebSocket connections between the frontend (packages/workshop-frontend) and backend (packages/workshop-backend). The RpcSession wraps the WebSocket transport and manages the lifecycle of these remote references.
Promise Pipelining for Zero-Round-Trip Calls
Cap'n Web eliminates network latency through promise pipelining. When an RPC method returns a stub wrapped in a Promise, you do not need to await the resolution before calling methods on the result. The protocol allows you to invoke methods directly on the Promise object; the server resolves the underlying stub before executing the pipelined arguments.
As implemented in the agent bridge code (packages/workshop-backend/src/agent.ts), this optimization removes an extra round-trip between the browser iframe and the edge Durable Object. The server-side resolution happens transparently before the arguments are delivered to the final target.
Security Boundaries and Prototype Protection
Security is enforced at the serialization layer. Cap'n Web automatically strips prototype-shadowing keys (such as __proto__ and toJSON) from objects traveling over the wire, ensuring JSON-compatible payloads cannot manipulate receiver prototypes.
The browser-side implementation runs inside a sandboxed iframe that Cloudflare injects via postMessage(). This iframe has no direct network access; all communication flows through the injected RPC channel, creating a strict capability boundary between untrusted gadget code and the host system.
How Cloudflare OS Implements Cap'n Web RPC
Cloudflare OS uses Cap'n Web as the universal glue connecting UI components, agent processes, and gatekeeper services.
Frontend-to-Backend Communication
The single-page application in packages/workshop-frontend establishes a persistent WebSocket connection to the Workshop kernel. The connection initialization pattern appears in packages/workshop-frontend/src/rateLimitedCapability.ts, where the frontend constructs an RpcSession over the WebSocket:
import { RpcSession, RpcTarget } from "capnweb";
const ws = new WebSocket(`${location.origin}/api`);
const session = new RpcSession(ws);
export const workshopApi: RpcTarget = session.exportTarget({
// Methods defined in packages/workshop-shared/src/api.ts
});
This exported stub exposes kernel methods (such as getUserProfile and submitChange) to sandboxed contexts while maintaining type safety through shared TypeScript interfaces.
Agent-to-Gadget Bridge
When users open gadgets, the browser loads sandboxed iframes that receive a gadget stub pointing to a Durable Object on the edge. In packages/workshop-backend/src/agent.ts (lines 700-717), the backend injects this RpcTarget into the iframe context. Calls on the stub automatically route over Cap'n Web, allowing gadget code to invoke server-side methods as if they were local functions.
The iframe environment cannot access the network directly; all database queries, storage operations, and external API calls must flow through this capability channel.
Type-Safe API Definitions
The shared package (packages/workshop-shared/src/api.ts) defines the canonical RPC interfaces imported by both client and server. This guarantees that the wire format matches TypeScript signatures across the frontend, backend, and gatekeeper workers (Context, Scheduler, and Cloudflare-Observability services).
Implementation Examples
Creating a Cap'n Web Session
Establishing communication requires wrapping a WebSocket transport with RpcSession and exporting capability targets:
import { RpcSession, RpcTarget } from "capnweb";
// Persistent WebSocket to the backend router
const ws = new WebSocket(`${location.origin}/api`);
const session = new RpcSession(ws);
// Export methods that gadgets can invoke
export const api: RpcTarget = session.exportTarget({
async getUserProfile() { /* ... */ },
async submitChange(data) { /* ... */ }
});
This pattern mirrors the production implementation in packages/workshop-frontend/src/rateLimitedCapability.ts (lines 7-13).
Leveraging Promise Pipelining
Gadget code can chain remote calls without awaiting intermediate stubs:
// Inside the sandboxed iframe
const gadget: RpcTarget = parent.gadget; // Injected by backend
// getSession returns Promise<RpcTarget>; pipeline the next call
const sessionPromise = gadget.getSession();
const docs = await sessionPromise.listDocuments(); // Zero extra round-trip
The server resolves getSession and executes listDocuments in a single network hop, as documented in AGENTS.md (lines 98-100).
Explicit Resource Disposal
While Cap'n Web automatically disposes stub arguments when RPC calls complete, long-lived objects (such as those stored in React state) require manual cleanup:
async function queryDatabase() {
const conn = await gadget.connectDatabase(); // Returns stub
await conn.query("SELECT * FROM notes");
// Free edge resources immediately
conn[Symbol.dispose]();
}
The GadgetUI.tsx component in the frontend demonstrates patterns for wrapping stubs before using them with useState to ensure proper disposal timing.
Key Source Files in cloudflare/cloudflare-os
| File | Purpose |
|---|---|
AGENTS.md |
Protocol overview, promise pipelining rules, and stub disposal guidelines |
packages/workshop-shared/src/api.ts |
Canonical TypeScript interfaces for type-safe RPC |
packages/workshop-shared/node_modules/capnweb/README.md |
Cap'n Web API documentation and serialization rules |
packages/workshop-frontend/src/rateLimitedCapability.ts |
Frontend WebSocket session initialization |
packages/workshop-frontend/src/GadgetUI.tsx |
React integration patterns for RPC stubs |
packages/workshop-backend/src/agent.ts |
Agent-side stub injection and iframe communication |
packages/workshop-backend/src/overseer.ts |
Kernel session management for gatekeepers |
Summary
- Cap'n Web RPC provides object-capability remote procedure calls over WebSockets, enabling secure communication between browser sandboxes and Cloudflare edge infrastructure.
- Promise pipelining eliminates network round-trips by allowing method calls on unresolved stubs, minimizing latency in gadget interactions.
- Automatic prototype stripping and iframe sandboxing create defense-in-depth security for untrusted code execution.
- Shared TypeScript definitions in
packages/workshop-shared/src/api.tsensure type safety across the entire Cloudflare OS stack. - Explicit disposal via
Symbol.dispose()prevents resource leaks in long-lived Durable Object stubs.
Frequently Asked Questions
How does Cap'n Web RPC differ from standard HTTP REST APIs?
Cap'n Web RPC uses persistent WebSocket connections and object-capability semantics rather than stateless request-response cycles. Methods appear as local JavaScript objects (stubs) rather than requiring manual HTTP client construction, and promise pipelining allows chaining dependent calls without waiting for intermediate network round-trips.
Can Cap'n Web RPC work outside of Cloudflare Workers?
While optimized for Cloudflare's edge runtime, the protocol runs over standard WebSockets and can operate in any environment supporting the capnweb library. However, the specific security model (sandboxed iframes with postMessage injection) relies on Cloudflare OS's architecture.
What happens if I don't dispose of RPC stubs manually?
Cap'n Web automatically disposes stubs when the RPC call that created them completes. However, storing stubs in React state or global variables creates long-lived references that persist beyond single calls. Without manual Symbol.dispose(), these stubs consume memory and capability slots on the edge Durable Object until garbage collection occurs.
Where is the Cap'n Web protocol specification defined?
The canonical documentation resides in packages/workshop-shared/node_modules/capnweb/README.md within the repository. Additionally, AGENTS.md documents Cloudflare OS-specific conventions for promise pipelining, stub disposal, and security boundaries that extend the base protocol.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →