Cap’n Web pipeline experiment
Status: SHIPPED read-only root capability tree, HTTP batching, WebSocket transport, WebRTC adapter, official SQLite WASM MessagePort body, local resume intent/outbox, instrumentation, and failure tests. PREVIEW-GATED authenticated CloudMailbox effects. PLANNED cockpit mailbox migration and additional carrier schedulers.
Cap’n Web is the live conversation between bodies. It is not the durable memory.
CURSO/0 signed cursor and capability records
│
▼
local SQLite semantic intent + outbox row DURABLE
│
▼
Cap’n Web MessagePort / HTTP batch / WebRTC PIPELINED
│
▼
existing CloudMailbox policy and SQLite effect DURABLE
│
▼
receipt linked to the local outbox row DURABLE
Transport is not identity. Identity is not authority. An RPC capability reference is not a mailbox, cursor, or Self identity. Losing the RPC session may discard live stubs; it must not discard the signed record, local intent, checkpoint, cloud effect, or receipt needed to resume.
One endpoint, two small capability trees
Ordinary GET / and HEAD / still serve the cockpit. Same-origin POST / is the universal HTTP-batch path; a WebSocket upgrade at the same root is optional. There is no second experimental HTTP endpoint and no global pipeline Durable Object.
The remote tree is:
CursorsApi.fabric() / mailbox(handle) / cursor(id)
Fabric.resolve(pointer) / place(requirements)
Mailbox.head(proof) / sync(after, proof) / append(envelope, proof) / receipt(id, proof)
CursorBody.resume(signedCursor) / checkpoint()
The dedicated SQLite Worker exposes only body operations over upstream Cap’n Web MessagePort:
LocalBody.describe()
LocalBody.mailbox(handle)
LocalBody.cursor(id)
LocalBody.compileLua(source)
Mailbox.head() / sync(after) / enqueue(envelope) / receipt(id[, accepted])
CursorBody.checkpoint() / enqueueResume(cursor, proof)
The local tree wraps the official @sqlite.org/sqlite-wasm Worker and backend registry from the SQLite body foundation. It does not create a second connection owner, schema, dependency, or VFS policy. Schema setup and ATTACH remain internal. Callers never receive raw write SQL.
SemanticOutboxScheduler recovers the pending envelope through those capabilities and selects the first available named carrier. HTTP batch ships now; LAN, WebRTC, and REST fallback adapters can implement the same bounded delivery interface. It intentionally does not race carriers after an ambiguous partial effect: the durable outbox remains pending until a fresh proof can read back or retry the idempotent envelope.
Complete path
The browser experiment at /capnweb.html exercises the shape below:
MessagePort
→ cursor.enqueueResume(...)
→ mailbox.db transaction
cursor_resume_intents + cursor_outbox
→ POST / Cap’n Web batch
mailbox.append(envelope, signedDeliverProof)
mailbox.receipt(append.id, signedSyncProof) // no intermediate await
→ existing CloudMailbox deliver policy
→ sender outbox-mirror read-back
→ local mailbox.receipt(effectId, accepted)
→ local outbox complete
The two short-lived proofs use the existing Cursors-Envelope format and are signed for their exact REST-equivalent method and path. The edge adapter forwards them into the existing CloudMailbox router. That path still verifies Ed25519 mailbox identity, consumes nonces, checks persisted sender/recipient profiles, applies friend/stranger rate policy, and enforces mailbox/rule quotas. Cap’n Web does not mint a new grant format.
Remote effects require CURSORS_CAPNWEB_WRITE_MODE=preview-authenticated. Both production and the ordinary stateless PR-preview configurations set it to disabled. wrangler.capnweb-preview.jsonc is the explicit route-free opt-in: it uses the separate curso-rs-capnweb-preview Worker name and therefore separate preview Durable Object namespaces. Read-only root capabilities remain available without write authority.
Bounds before amplification
Every batch reserves limits before expensive work:
- streamed request body: 64 KiB before RPC parsing;
- Cap’n Web depth: 32; bigint digits: 256;
- per HTTP batch, or per 10-second WebSocket window: 32 stages; 8 expensive stages;
- application argument bytes: 64 KiB;
- appended envelope: 48 KiB;
- returned sync deltas: 200;
- worst-case Durable Object calls reserved: 32 per batch.
An append reserves 28 calls before storage: directory checks, target mailbox, sender mirror, and the existing maximum of 24 reply rules. A dependent receipt reserves two more. The instrumentation reports the conservative reservation as 30. Existing CloudMailbox per-sender hourly limits, inbox/mirror quotas, rule limits, nonce replay checks, and expiry still apply inside that ceiling.
Pipelined versus durable
Results include an instrumentation record. pipelined means live scheduling, lookup, verification, or transport. durable is emitted only after a local SQLite transaction, CloudMailbox acceptance, sender-mirror read-back, or local receipt transaction.
The partial-failure test lets CloudMailbox accept the append, then supplies a receipt proof signed for the wrong method. The second stage fails, the local outbox remains pending, and a fresh Cap’n Web session with a new correct proof recovers the sender-mirror receipt. Byte-identical local retries are idempotent; conflicting reuse of a cursor, envelope, effect, or receipt ID aborts the transaction.
Measurements
Run npm run measure:capnweb for the deterministic protocol-body harness. For CloudMailbox append → sender-mirror receipt:
| Measurement | current REST | Cap’n Web HTTP batch |
|---|---|---|
| browser round trips / Worker requests | 2 | 1 |
| request body bytes | 113 | 1,113 |
| response body bytes | 439 | 1,979 |
| independently gzipped protocol bodies | 469 | 1,186 |
| simulated 40 ms RTT | 80 ms | 40 ms |
| simulated 150 ms RTT | 300 ms | 150 ms |
| worst-case DO calls reserved | 30 | 30 |
The result is intentionally unromantic: batching halves the network latency turns but costs more bytes, and it does not erase backend policy work.
The current production Vite build reports:
| Browser asset | raw | gzip |
|---|---|---|
| isolated Cap’n Web demo entry | 4.13 KiB | 1.86 KiB |
| shared identity/proof helper chunk | 65.59 KiB | 18.78 KiB |
| official SQLite WASM | 864.75 KiB | 401.93 KiB |
| dedicated SQLite body Worker | 303.03 KiB | build output does not report gzip |
The cockpit’s main entry does not absorb the SQLite Worker/WASM payload; the demo loads it only when the local body is opened.
SQLite and VFS boundary
Local writable state remains on official SQLite WASM opfs, opfs-sahpool, or memory backends selected once by the Worker. Local self.db, mailbox.db, world.db, cache.db, and outbox.db may be attached to that connection. A Cloudflare Durable Object database is never attached remotely. Only semantic envelopes, tombstones, cursor records, checkpoints, and receipts cross carriers—never WAL frames or database pages.
Read-only fetched snapshots are a separate follow-up. sql.js-httpvfs and its 2021 HTTP-range design remain valuable prior art, but neither they nor wa-sqlite or sqlite-wasm-http are declared fastest before a common Chromium workload exists.
Explicit exclusions
- CURSO/0 signed/hashable records are unchanged.
protocol/islands.jsremains an independent framing experiment.- issue #39 SignedData/SignedForm profiles are not part of this change.
- arbitrary remote SQL, page/WAL replication, Nelua execution, and a new pipeline Durable Object are absent.