capnweb-go: Cap'n Web RPC for Go

capnweb-go: Cap'n Web RPC for Go
↑ Top

I’ve been working on flaticols/capnweb-go — a Go implementation of Cloudflare’s Cap’n Web RPC protocol.

What is Cap’n Web?

Cap’n Web is a JSON-based, bidirectional RPC protocol designed by Kenton Varda — the same person who built Cap’n Proto and the Cloudflare Workers runtime. It’s the RPC layer underneath Workers, designed for the web: WebSockets, HTTP batch requests, or any message-passing transport.

The headline features:

  • Bidirectional — either side can call the other; no fixed client/server roles
  • Promise pipelining — chain dependent calls without waiting for intermediate results, collapsing multiple round trips into one
  • Pass-by-reference — export any object as a stub and call it remotely, with automatic reference counting
  • Streaming — multiplexed readable/writable streams over a single connection

Not Cap’n Proto

The names are similar and the author is the same, but these are different protocols. Cap’n Proto is a binary, schema-driven, systems-level RPC framework. Cap’n Web trades raw throughput for web-native simplicity: JSON wire format, no .capnp schemas, no code generation, methods discovered at runtime.

Both share the capability-based object model and promise pipelining. Cap’n Web is just designed for a different environment.

Using it in Go

Expose any struct as an RPC target:

type Greeter struct {
    capnweb.RpcTargetBase
}

func (g *Greeter) Greet(_ context.Context, name string) (string, error) {
    return "Hello, " + name + "!", nil
}

Serve over WebSocket:

mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    tr, _ := capnweb.WSAccept(w, r, &capnweb.WSAcceptOptions{Origins: []string{"*"}})
    sess := capnweb.NewSession(tr, &Greeter{})
    sess.Run(r.Context())
})

Call it from Go:

tr, _ := capnweb.WSDial(ctx, "ws://localhost:8080/ws", nil)
client := capnweb.NewSession(tr, nil)
go client.Run(ctx)

result, _ := capnweb.Call[string](ctx, client.Main(), "Greet", "World")
// "Hello, World!"

Or from TypeScript on a Cloudflare Worker:

import { newWebSocketRpcSession } from "capnweb";

const stub = newWebSocketRpcSession("ws://localhost:8080/ws");
const result = await stub.Greet("World");

No schemas. No generated code. Just methods.

Promise pipelining

The more interesting feature — chain calls without waiting for each result:

main := client.Main()
calc, _ := main.Pipeline(ctx, "GetCalculator")  // no round trip yet
result, _ := capnweb.Call[float64](ctx, calc, "Add", 3.0, 4.0) // one round trip for both
defer calc.Release(ctx)

The two calls are sent together and resolved in a single round trip, even though Add depends on the result of GetCalculator.

Status

All core and advanced protocol features are implemented — pipelining, pass-by-reference, streaming, HTTP batch transport. Cross-language interoperability with the TypeScript reference implementation is validated by an automated test suite. Check out flaticols/capnweb-go for the full details.

Discussion

Loading replies…
Reply on Bluesky