Skip to content

SYSTEM Cited by 2 sources

Cap'n Web

Cap'n Web is a pure-TypeScript object-capability RPC library from Cloudflare (GitHub, MIT, announced 2025-09-22). Minify+gzip is under 10 kB with no dependencies; runs in every major browser, Workers, Node.js, and other modern JS runtimes; ships transports for HTTP batch, WebSocket, and postMessage() out of the box, and is extensible to others.

What it is

A JS-native RPC protocol that inherits the capability + promise-pipelining design of Cap'n Proto but drops the schema language and binary wire format. Instead:

  • No schemas, no codegen. You define RPC servers by extending RpcTarget; you call remote methods on a client stub as if it were a local object. TypeScript interfaces can give you end-to-end type safety at compile time.
  • Wire format is JSON post-processed for escape-array encoding of non-JSON types (["date", 1758...] is a Date; [["Alice","Bob"]] is a literal array). Human-readable; you can tail -f a session and understand it.
  • Symmetric protocol. No client/server distinction at the protocol level — both peers maintain import + export tables and can invoke each other's exported objects.

The four-message protocol

Message Meaning
push Evaluate an expression; store result in the peer's export table at a predictable positive ID.
pull Please serialize and send back the value at this ID (issued only if the caller actually awaits).
resolve Here's the value (in response to a pull).
reject Call threw / rejected.

The caller predicts the positive export ID of each push — that's what enables promise pipelining without waiting for the resolve. See the protocol spec for details.

Capability features (vs typical RPC)

  • Pass-by-reference for functions. Passing a function over RPC gives the recipient a stub; invoking the stub makes an RPC back to the creator → bidirectional RPC with callbacks as first-class arguments.
  • Pass-by-reference for objects. Classes extending RpcTarget are passed by reference; method calls route back to the origin.
  • Promise pipelining. The Promise returned by a call is a JS Proxy — methods invoked on it are sent immediately as speculative dependent calls.
  • Capability-based security. authenticate() returning a session stub is the idiomatic auth pattern. The session object is unforgeable at the protocol layer — clients can only obtain one via a successful authenticate() call.

The .map() trick

Cap'n Web supports promise.map(callback) for pipelined transformations over a promised array — this is the feature that makes it a serious alternative to GraphQL for the canonical "N+1 query" case. The callback is run once client-side against a Proxy placeholder (must be synchronous); the interceptor records the pipelined calls as a DSL expression that is just the RPC protocol itself, and ships it to the server to replay per array element. No round trips, no schema. (patterns/record-replay-dsl)

Setup

Client (WebSocket):

import { newWebSocketRpcSession } from "capnweb";
const api = newWebSocketRpcSession("wss://example.com/api");
const result = await api.hello("World");

Client (HTTP batch — once you await, the batch is done):

const batch = newHttpBatchRpcSession("https://example.com/api");
const [a, b] = await Promise.all([batch.hello("Alice"), batch.hello("Bob")]);

Server (Cloudflare Worker):

import { RpcTarget, newWorkersRpcResponse } from "capnweb";
class MyApiServer extends RpcTarget {
  hello(name) { return `Hello, ${name}!` }
}
export default {
  fetch(req) {
    if (new URL(req.url).pathname === "/api")
      return newWorkersRpcResponse(req, new MyApiServer());
    return new Response("Not found", {status: 404});
  }
}

Production use

  • Wrangler remote bindings (GA 2025-09-16) — local workerd test instances speak Cap'n Web RPC to production services. First shipped consumer, announced ~6 days before the launch post.
  • Cloudflare states internal experiments in frontend apps.

Trade-offs

  • Experimental. Cloudflare explicitly flags "willingness to live on the cutting edge may be required."
  • No runtime type checking. TypeScript is compile-time only; malicious clients can still send wrong-shaped args — pair with runtime validators like Zod at handlers.
  • .map() lambda must be synchronous. Any await inside the callback breaks the DSL capture. Users will hit this and have to restructure.
  • Capability lifetime / GC. A stub held by a remote peer pins the underlying object. The post notes IDs are never reused but doesn't walk through the disposal discipline — a real consideration for long-lived WebSocket sessions.

Seen in

As a TypeScript-as-schema precedent

Cap'n Web's "define RPC in TypeScript types, not a schema DSL" posture is the direct precedent for Cloudflare's 2026-04-13 decision to make TypeScript the source of truth for generating the cf CLI, Workers bindings, SDKs, Terraform provider, and MCP Code Mode server from one unified interface schema. See patterns/typescript-as-codegen-source (Source: sources/2026-04-13-cloudflare-building-a-cli-for-all-of-cloudflare).

Last updated · 200 distilled / 1,178 read