RepoCloudflare (Workers AI)Cloudflare (Workers AI)published Jun 8, 2025seen 5d

cloudflare/capnweb

TypeScript

Open original ↗

Captured source

source ↗
published Jun 8, 2025seen 5dcaptured 8hhttp 200method plain

cloudflare/capnweb

Description: JavaScript/TypeScript-native, low-boilerplate, object-capability RPC system

Language: TypeScript

License: MIT

Stars: 3834

Forks: 133

Open issues: 28

Created: 2025-06-08T02:51:20Z

Pushed: 2026-06-09T02:01:11Z

Default branch: main

Fork: no

Archived: no

README:

Cap'n Web: A JavaScript-native RPC system

Cap'n Web is a spiritual sibling to Cap'n Proto (and is created by the same author), but designed to play nice in the web stack. That means:

  • Like Cap'n Proto, it is an object-capability protocol. ("Cap'n" is short for "capabilities and".) We'll get into this more below, but it's incredibly powerful.
  • Unlike Cap'n Proto, Cap'n Web has no schemas. In fact, it has almost no boilerplate whatsoever. This means it works more like the JavaScript-native RPC system in Cloudflare Workers.
  • That said, it integrates nicely with TypeScript.
  • Also unlike Cap'n Proto, Cap'n Web's underlying serialization is human-readable. In fact, it's just JSON, with a little pre-/post-processing.
  • It works over HTTP, WebSocket, and postMessage() out-of-the-box, with the ability to extend it to other transports easily.
  • It works in all major browsers, Cloudflare Workers, Node.js, Bun, Deno, and other modern JavaScript runtimes.

The whole thing compresses (minify+gzip) to under 10kB with no dependencies.

Cap'n Web is more expressive than almost every other RPC system, because it implements an object-capability RPC model. That means it:

  • Supports bidirectional calling. The client can call the server, and the server can also call the client.
  • Supports passing functions by reference: If you pass a function over RPC, the recipient receives a "stub". When they call the stub, they actually make an RPC back to you, invoking the function where it was created. This is how bidirectional calling happens: the client passes a callback to the server, and then the server can call it later.
  • Similarly, supports passing objects by reference: If a class extends the special marker type RpcTarget, then instances of that class are passed by reference, with method calls calling back to the location where the object was created.
  • Supports promise pipelining. When you start an RPC, you get back a promise. Instead of awaiting it, you can immediately use the promise in dependent RPCs, thus performing a chain of calls in a single network round trip.
  • Supports capability-based security patterns.

Installation

Cap'n Web is an npm package.

npm i capnweb

Example

A client looks like this:

import { newWebSocketRpcSession } from "capnweb";

// One-line setup.
let api = newWebSocketRpcSession("wss://example.com/api");

// Call a method on the server!
let result = await api.hello("World");

console.log(result);

Here's the server:

import { RpcTarget, newWorkersRpcResponse } from "capnweb";

// This is the server implementation.
class MyApiServer extends RpcTarget {
hello(name) {
return `Hello, ${name}!`
}
}

// Standard Cloudflare Workers HTTP handler.
//
// (Node and other runtimes are supported too; see below.)
export default {
fetch(request, env, ctx) {
// Parse URL for routing.
let url = new URL(request.url);

// Serve API at `/api`.
if (url.pathname === "/api") {
return newWorkersRpcResponse(request, new MyApiServer());
}

// You could serve other endpoints here...
return new Response("Not found", {status: 404});
}
}

More complicated example

Here's an example that:

  • Uses TypeScript
  • Sends multiple calls, where the second call depends on the result of the first, in one round trip.

We declare our interface in a shared types file:

interface PublicApi {
// Authenticate the API token, and returned the authenticated API.
authenticate(apiToken: string): AuthedApi;

// Get a given user's public profile info. (Doesn't require authentication.)
getUserProfile(userId: string): Promise;
}

interface AuthedApi {
getUserId(): number;

// Get the user IDs of all the user's friends.
getFriendIds(): number[];
}

type UserProfile = {
name: string;
photoUrl: string;
}

(Note: you don't _have to_ declare your interface separately. The client could just use import("./server").ApiServer as the type.)

On the server, we implement the interface as an RpcTarget:

import { newWorkersRpcResponse, RpcTarget } from "capnweb";

class ApiServer extends RpcTarget implements PublicApi {
// ... implement PublicApi ...
}

export default {
async fetch(req, env, ctx) {
// ... same as previous example ...
}
}

On the client, we can use it in a batch request:

import { newHttpBatchRpcSession } from "capnweb";

let api = newHttpBatchRpcSession("https://example.com/api");

// Call authenticate(), but don't await it. We can use the returned promise
// to make "pipelined" calls without waiting.
let authedApi: RpcPromise = api.authenticate(apiToken);

// Make a pipelined call to get the user's ID. Again, don't await it.
let userIdPromise: RpcPromise = authedApi.getUserId();

// Make another pipelined call to fetch the user's public profile, based on
// the user ID. Notice how we can use `RpcPromise` in the parameters of a
// call anywhere where T is expected. The promise will be replaced with its
// resolution before delivering the call.
let profilePromise = api.getUserProfile(userIdPromise);

// Make another call to get the user's friends.
let friendsPromise = authedApi.getFriendIds();

// That only returns an array of user IDs, but we want all the profile info
// too, so use the magic .map() function to get them, too! Still one round
// trip.
let friendProfilesPromise = friendsPromise.map((id: RpcPromise) => {
return { id, profile: api.getUserProfile(id) };
});

// Now await the promises. The batch is sent at this point. It's important
// to simultaneously await all promises for which you actually want the
// result. If you don't actually await a promise before the batch is sent,
// the system detects this and doesn't actually ask the server to send the
// return value back!
let [profile, friendProfiles] =
await Promise.all([profilePromise, friendProfilesPromise]);

console.log(`Hello, ${profile.name}!`);

// Note that at this point, the `api` and `authedApi` stubs no longer work,
// because the batch is done. You must start a new batch.

Alternatively, for a long-running interactive application, we can set up a persistent…

Excerpt shown — open the source for the full document.

Notability

notability 6.0/10

High-starred new repo by major org.