A hotel guest-relations team ran their daily rounds off a 131-task, 11-section checklist that had already survived one rebuild, from a paper PDF into a single-file web app.
End-to-end: research, flow architecture, high-fidelity UI, design system contribution.
Patch-Level Merge, Not Full-State Overwrite
4/4: Cross-device sync checks, proven not assumed
Scope, Constraints & Reality
Everyone on a shift opens the same URL and sees the same checklist. Real per-user accounts would have added a login flow to a tool used with wet hands and a phone propped on a cart — the access gate deters casual entry, and the sync layer doesn't try to be more than that.
The original single-device app worked with zero backend. Sync had to be strictly additive: every fetch degrades silently on failure, so a bad wifi patch means the checklist keeps working exactly as it did before sync existed, just without other phones' edits until the network comes back.
Cloudflare Pages Functions plus one KV namespace, chosen specifically because there's no server to patch, scale, or forget about, for a solo-maintained staff tool where 'someone keeps this running' was never going to be a real answer.
Every line of the sync engine, the two Cloudflare Functions, and the cross-device Playwright test came from specifying intent and reviewing the result. The same agent then created the KV namespace, set the secrets, and worked through a live deploy failure by failure until it actually shipped.
Design Decisions
Patch-Level Merge, Not Full-State Overwrite
Two phones editing the same day at once ruled out “last full save wins” — one device's photo count would silently erase another's just-typed remark. The server merges per field instead: a tick, a remark, a summary line each write independently, so two people editing different parts of the same round within the same second never step on each other. One KV namespace and two small Functions handle this; Durable Objects would have added real-time guarantees this app, at hotel-shift scale, never needed.
A Pending Edit Always Beats A Stale Poll
Every device polls the server every 20 seconds. Without a rule, a poll landing mid-typing would overwrite a keystroke with whatever the server last had. The fix: any field with an edit still queued to send wins over incoming server data for that same field, unconditionally, until the send confirms. It's the one rule the whole sync engine depends on, and the one thing every mutation site in the app had to be threaded through consistently.
Photos Stay On The Phone That Took Them
Only a photo's id, timestamp and size sync, never the bytes. Uploading real images would mean object storage, a signed-upload endpoint, and a real answer for “whose photo is this” across devices, none of which the brief actually needed. What syncs is enough for every device to know a photo exists and when it was taken; opening it means being on the phone that took it — a deliberate boundary, not a missing feature.
Impact
A second Playwright suite opens two separate browser contexts as a stand-in for two phones and checks that a tick and a remark made on one appear on the other, in both directions, plus that a day's data is visible server-side the way the archive depends on. All four checks pass against the real Functions + KV stack, not a mock.
Only id, timestamp and size sync for a photo. The image itself never crosses the network — verified by design rather than by omission, since there's no upload endpoint for it to reach.
An 800ms debounce on the writing device, then a network round trip to Cloudflare KV. The other phone sees it on its next 20-second poll, or immediately if it's mid-unlock, which triggers an out-of-cycle fetch.


