capnwasm

Real Cap'n Proto C++ in the browser. Typed RPC, binary wire.

One typed-client toolchain for any backend — write a .capnp schema, get a fast typed JS client back. npx capnwasm gen user.capnp emits a reader/builder that runs against real upstream Cap'n Proto C++ statically compiled to wasm.

Not production-ready yet. capnwasm is intended to become production-capable, and normal readers now keep message bytes in managed WebAssembly.Memory. Treat these pages as controlled demos while allocator, large-data, hostile-input, and secure-memory hardening continues.

What this project is

capnwasm is me trying to learn Cap'n Proto and capnweb by building the opposite experiment: keep the real Cap'n Proto binary wire in the browser, compile the upstream C++ runtime to wasm, and measure what changes.

This is not a scoreboard where Cap'n Proto always wins. The useful question is the tradeoff boundary: when does zero-copy / sparse access / raw binary pay off, and when is JSON or capnweb the better choice?

JSON / capnweb is usually better for
  • tiny JS-to-JS objects
  • control-plane calls where payload is a few bytes
  • smallest bundle / no wasm runtime
Cap'n Proto / capnwasm is interesting for
  • raw binary data without base64
  • large payloads where you read only a few fields
  • wire interop with non-JS Cap'n Proto services

Live comparison

First this page runs a small fetch/decode/render pass using the same fixture rows as the playground. Then it starts the RPC smoke against the current Worker.

1. Fetch playground smoke: same data, three encodings

REST / JSON measuring… time: —
capnweb measuring… time: —
capnwasm measuring… time: —

Running fetch smoke…

2. RPC smoke: runs after payload sizes load

RPC burst, 200 concurrent calls (browser→Worker WS) waiting for payloads… live
RPC 64 KB binary echo (browser→Worker WS) waiting for payloads… live
Bundle — full RPC client (JS + wasm, gzip) loading… build
Wasm init on this page (fetch + compile) measuring… live
📊
Render bench
End-to-end full pipeline: capnweb × capnwasm crossed with WebSocket × HTTP-batch, three sizes, cold & warm. Both libraries win some, lose some — the whole picture.
Tradeoffs
📖
Reader access snippets
Read-only TypeScript snippets for draft(), direct getters, toObject(), and builder edits. Each snippet runs against real Cap'n Proto bytes in your browser.
Snippets
🌍
A world where everyone speaks Cap'n Proto
A globe of Cloudflare PoPs. Send a message; every PoP runs a different language — TS, Python, Ruby, Go, Java — on its own Worker. Same wire bytes encoded by capnp.slim.wasm, every runtime decodes them, every reply bubbles up at its real Cloudflare location.
Live demo
💬
Live chat demo
createClient + subscribeQuery + optimistic in a working chat. Open in two tabs to watch messages stream live across.
Interactive
🧩
Dynamic schema demo
No codegen: define the schema as JS data, build Cap'n Proto bytes, pick selected fields, and round-trip through the Worker.
No codegen
🔁
OpenAPI conversion
Paste OpenAPI JSON, convert to manifest, emit canonical .capnp, and round-trip back to canonical OpenAPI in the browser.
Contract
⚖️
vs capnweb
Where capnwasm wins, where it loses, where it’s tied. Reproducible numbers. Bundle size and cold start go to capnweb.
Tradeoffs
🔍
Wire inspector
One line in DevTools, walks any Cap'n Proto frame. Schemaless or schema-aware. Hosted as a single /inspect.js on this site — not in the npm package.
Debug tool
📝
Notes from the trenches
Field report on the perf work: allocation traps, GC churn, a fast-path that wasn't, one negative SIMD result, and the tradeoff summary.
Write-up