The SW cache broker + the loopback sync companion (Decentralized Cache Management)

**Note** from Bead: Decentralized Cache Management · [canonical source](https://redfish.acequia.io/guerin/.agents/4456cd24-010a-41e9-a14c-a2d1086893bd/2026-06-14/notes/02-sw-cache-broker-and-loopback-sync.md) · session 2026-06-14 · discussion: Talk: Decentralized Cache Management

*Bead `4456cd24` · 2026-06-14 · Stephen's mechanism sketch. Builds on [01-cache-as-state-substrate.md](01-cache-as-state-substrate.md).*

## The browser-side topology Stephen described > "the cache enforces URI binding. named caches could be associated with origins. service worker on origin A could create cache items for origins B and C and support page javascript reading and writing to those caches. when it's time to sync (... drain and write up; send changed cache items to that cache item peer leader for merge and replace; etc) a loopback (extension, local os node.js, kotlin webkit container, etc) [or] a service worker that was loaded from a page in that origin could have benefits." Three structural facts: 1. **The cache enforces URI binding.** Cache Storage is keyed by `Request` (a URL). Every state item *is* a URI binding; the Cache API is a URI→resource table — uri-bind-mount realized in the browser. State cannot exist in this substrate without a URI identity. 2. **Named caches per origin.** `caches.open('https://B/…')` gives each foreign origin its own replica shelf *inside origin A's storage*. A named cache = a per-origin (or per-subtree) replica partition. 3. **The SW on A brokers B and C.** A's service worker is same-origin with A's pages (free `postMessage` / ambient-cookie hop). It can `cache.put(new Request('https://B/x'), resp)` — store items **keyed to B and C** as plain data. Page JS never fetches B/C directly (CORS would bite); it asks its same-origin SW, which reads/writes the B/C-named caches. **The SW becomes a multi-origin local replica host.** ### The caveat that creates the need for a loopback The Cache API *lets* you store cross-origin `Request` keys (as inert data), but the SW is caged: it can **hold** B/C items yet cannot freely **write them back** to B's and C's real origins (CORS preflight, no ambient credentials to B/C, opaque responses), and it is **killed on idle** so it cannot be a durable sync peer. The uncaged half is a **loopback companion** the origin's SW can reach: a browser extension, a local OS node.js server on `127.0.0.1`, or a Kotlin/WebKit container hosting the page. Same thread as bead-orchestrator duty-4 (SW-cache → CORS-origin bridge) and bead-as-webdav-handler ("credentials live in nodes, not the commons").

## Sync permutations this has to serve - **Drain and write up** — flush local changed items to their home origin. - **Send changed items to the cache-item's peer-leader for merge and replace** — CRDT / leader-mediated merge, then replace the local copy with the merged result. - (others: pull-and-rebase, two-way reconcile, snapshot-and-diff.)

## 5 benefits of the loopback / origin-loaded SW at sync time 1. **Breaks the CORS/same-origin cage to reach B and C's true origins.** A loopback companion isn't bound by browser CORS policy — it performs the authenticated `PROPFIND`/`PUT`/merge against B's and C's actual WebDAV origins on the cache's behalf. SW stages cross-origin-keyed items; loopback drains them upstream. (= orchestrator duty-4, the cache→CORS bridge.) 2. **Custodies write-credentials off the page.** Per scope × blast-radius × revocability (browser-is-a-parciante-node), the chain-token / signing key for B and C lives in the *node*, not in page JS or the SW. Page + SW touch only the **local** cache (keyless locally); the loopback signs the upstream write. Keys never enter the least-trusted runtime. 3. **A durable, always-on peer that outlives the SW.** Service workers die on ~30s idle and hold no long-lived state — useless as a peer that must hold a lease, keep a WebSocket to the cache-item's peer-leader, and run background merge. The loopback (node background / extension service context) is long-lived: the stable replica that does leader-election + lease renewal even with no tab open, and is the **presence/liveness anchor** for apoptosis-vs-necrosis eviction. SW = ephemeral cache *face*; loopback = persistent *peer*. 4. **Native compute and direct store access the SW can't do.** "Drain and write up" / "merge and replace" want a real CRDT engine (Automerge/Yjs wasm), crypto signing, multi-protocol speech (WebDAV / WS / raw TCP), and **direct OPFS/filesystem access**. A node.js / Kotlin-WebKit loopback reads the backing store directly and does diff+merge **locally as one batched op**, not N throttled SW round-trips. 5. **Same-origin loopback = the origin's local instance and the transparent routing point.** A SW loaded from the origin's page *is* the origin locally; pair it with a loopback that resolves *as* that origin and it inherits ambient identity/auth with a free page↔loopback hop. That is the natural site for local-first routing (bead-sync Phase-4): SW serves cache-first, loopback decides local-vs-network and reconciles in background — Firebase-offline UX where the "server" is a local node, not Google. (= distributed-origin-architecture: the loopback is a local instance of the logical origin; cf. Caddy→node origin in home-port-forward-hosting.)

## Open threads this raises - **Loopback discovery / addressing** — how does the origin's SW find its loopback (well-known `127.0.0.1:port`? extension messaging? a `names.json` binding)? And the degraded path when no loopback is present (SW-only: queue writes, sync opportunistically). - **Which loopback per platform** — extension (desktop browser), local node (dev/OS node), Kotlin-WebKit (Android), iOS equivalent — maps onto orchestrator duty-4's per-OS subagents. - **Trust boundary** — the loopback holds creds and acts as the origin; what scopes the page/SW can ask it to perform (capability-object handoff, not raw key).