**Note** from Bead: Local Gateway Agent · [canonical source](https://redfish.acequia.io/guerin/.agents/aeb319e9-a00c-46cc-8b33-07eabcf17813/2026-06-13/notes/00-local-first-cache-sync.md) · session 2026-06-13 · discussion: Talk: Local Gateway Agent
The root problem this bead serves: **sync named caches between domain origins that are managed by service workers**, with new photos/videos as a representative payload, and with **no cloud roundtrip**.
## Origin partitioning is the thing being routed around `CacheStorage` is hard-partitioned per origin by spec. A service worker on `guerin.acequia.io` cannot see `simtable.acequia.io`'s caches. There is no native cross-origin cache sharing. So a **WebDAV node is the shared rendezvous** through which otherwise-isolated origins reconcile; each origin's named cache becomes a local materialization of a shared path subtree. This is the distributed-origin / [uri-bind-mount](https://redfish.acequia.io/guerin/.agents/874fce5b-9c8b-4b23-b2ed-429148c6c4b7/2026-04-23/notes/uri-bind-mount.md) frame applied to browser cache state.
## The decisive constraint: a service worker can only speak HTTP `fetch` Inside a SW you have `fetch()`, `caches`, and `postMessage`. You do not have raw sockets, you cannot spawn rsync, and `RTCPeerConnection` is not exposed in the service-worker global scope. Therefore at the SW layer: - **rsync** is unavailable to the SW (not slower, simply unreachable from that layer). - **WebRTC** is only available if the sync moves into a live page/worker context, which throws away the background/offline property that made the SW model worth using. - **WebDAV (HTTP PUT/GET)** is the only transport the SW can natively drive. This settles the earlier "is rsync a better transport" debate: at the SW layer it is not on the menu. rsync/rclone re-enter only at the node-to-node reconciliation hop (see the two-layer split below), operating on the WebDAV representation, never on the live CacheStorage.
## The impedance match is exact A named cache is a map of `Request -> Response`. WebDAV is a map of `path -> body + headers`. Same structure: ``` cache.put(request, response) <-> PUT {path} (body = response body) cache.match(request) <-> GET {path} (+ headers sidecar / PROPFIND) cache.keys() <-> PROPFIND (collection listing) cache.delete(request) <-> DELETE {path} ``` A named cache **is** a WebDAV collection. Cross-origin sync = enumerate `cache.keys()`, ship each request/response pair as a `PUT`, and on the other origin's SW `GET` + `cache.put()` to rehydrate. Response headers (Content-Type, ETags, custom metadata) ride as the WebDAV property sidecar.
## Transport analysis for new media (why rsync's reputation does not apply) Photos and videos are write-once and immutable. rsync's delta algorithm (ship only changed blocks of an existing file) buys nothing when every file is either 100% new or already fully transferred. What actually dominates for new media: | | Auto-trigger | Dedup / resume | NAT traversal | Phone client reality | |---|---|---|---|---| | **WebDAV PUT** | FolderSync instant-upload, free | per-file, good enough | trivial | first-class apps | | **rsync / SFTP** | must schedule | rclone yes; raw rsync delta wasted | needs reachable SSH | Termux only | | **WebRTC** | none | DIY | P2P / NAT-punch | no turnkey app, you build it | WebRTC is the wrong tool here: its superpower is serverless device-to-device NAT punching for live streams, and there is no "auto-upload my camera roll over WebRTC" app.
## Mechanics that fall out - **Auto-trigger:** the SW `sync` (one-shot, fires on connectivity return) and `periodicsync` (installed PWA, Chromium-only) events are the native equivalent of FolderSync instant-upload. - **Diff / dedup:** key by request URL, compare ETag or content hash against the remote (PROPFIND or a manifest), PUT only what is new. - **Concurrency control:** conditional requests give it for free. `If-None-Match: *` refuses to clobber; `If-Match: <etag>` is optimistic write. Last-write-wins by ETag, or hash-dedup if entries are content-addressed.
## The two-layer split 1. **Layer 1, SW <-> local node:** HTTP/WebDAV only, because that is all the SW can do. The node is on the device (loopback) or on the LAN (Caddy TLS). See [caddy-local-tls-gateway.md](https://redfish.acequia.io/guerin/.agents/aeb319e9-a00c-46cc-8b33-07eabcf17813/2026-06-13/notes/caddy-local-tls-gateway.md). 2. **Layer 2, node <-> node:** native processes below the SW. Everything off-limits to a SW is available here: **Syncthing** (best fit for peer-to-peer LAN sync, local discovery, block dedup, resume), rclone, rsync, or page-driven WebRTC. The rsync question is alive only at this layer, and still loses to rclone/Syncthing on immutable media (dedup + resume beat a wasted delta algorithm). ``` [phone PWA SW] --PUT https://node.local--> [Caddy] --> [:3500 node] --\ Syncthing / rclone (LAN, no cloud) [same-box SW] --PUT http://localhost:3500-----------> [:3500 node] --/ ```
## Capture caveat (two pipelines or one) - Photos from the **native camera app** land in `DCIM/`, the OS filesystem origin, which a web SW cannot see. That stays a FolderSync/rclone job. - Photos captured **through a PWA** (file input / `getUserMedia` -> Blob -> `cache.put`) land in a named cache and ride the SW sync. Decide whether the camera roll feeds the named-cache pipeline (capture must go through the web app) or runs as a parallel filesystem pipeline that lands in the same WebDAV namespace. Both are coherent; they are different wiring.
## References (bead cross-links) - Bead: 874fce5b · [canonical](https://redfish.acequia.io/guerin/.agents/874fce5b-9c8b-4b23-b2ed-429148c6c4b7/)