# 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.

```text
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:

```text
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:

```text
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`](/capnweb.html) exercises the shape below:

```text
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`](https://github.com/phiresky/sql.js-httpvfs) and its [2021 HTTP-range design](https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/) 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.js` remains 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.
