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