Skip to content

Offline across tabs

Tabs on the same folder and user share one persisted store. This page is the contract for how they coexist: how each tab’s writes stay durable, how a dead tab’s writes are picked up, and how live edits reach a sibling with no server in the loop. It assumes offline persistence — the journal and the document cache are what all of it runs on.

Every tab journals; the dead ones are adopted

Section titled “Every tab journals; the dead ones are adopted”

Every tab is durable: each session journals under its own owner id and holds a Web Lock named for it, so live tabs never collide. The lock doubles as a liveness signal — when a tab goes away its lock frees and a surviving session adopts its records, on the next page load or through a periodic adoption sweep (every 15 seconds by default), so a dead sibling’s unsent writes replay without waiting for anyone to reload. Adoption is crash-safe in the store itself, and a frozen tab misjudged as dead only produces a double-replay the protocol absorbs: replayed writes re-acknowledge idempotently under their original event ids.

Offline tabs share Yjs edits through the cache

Section titled “Offline tabs share Yjs edits through the cache”

For the Yjs lane — and only that lane, since only it is a CRDT — the shared cache doubles as a local bus. A record landing in it is broadcast to the namespace’s other sessions, and each sibling merges its Yjs snapshots into the live documents it already knows. Since every flush re-encodes those live documents, unsent edits included, two offline tabs editing the same rich-text document converge with no server round-trip: type in one, watch it render in the other, both still offline. Merging is what makes duplicate or racing deliveries benign, and merged content is as durable as native content — the receiving tab’s snapshot includes it, so either tab’s reconnect pushes the edits to the server. An edit survives its author’s tab dying offline, without waiting for adoption.

The write journal broadcasts the same way, but pending JSON writes take the opposite discipline: mirror, never merge. They aren’t CRDTs — they are optimistic patches against a sequence, order-sensitive and rejectable, applied by the server in whichever order the tabs reconnect. So a sibling’s record is rendered — base, own pending, sibling pending — but never replayed, restamped, or merged into the receiving tab’s queue. Ownership moves exactly once, through adoption, when the author dies. When the author instead reconnects and its write acks, the record clears and the confirmed content arrives over the cache bus, which only advances the render forward.

All of it stays invisible to the app: the pending-write count and the status document answer for the whole namespace, so a “changes pending” indicator is truthful across tabs without the app learning whose queue holds a write. Live edits, offline creates (discoverable by name, since they surface in sys:index reads), deletes, and staged-session work are all tab-coherent offline.

Two caveats are inherent. Mirrored writes are another session’s unconfirmed work, and can bounce on that session’s reconnect exactly like your own optimistic writes. And identical renders across tabs are not guaranteed while offline: each tab composes its own pending writes first and the mirror on top, so disjoint edits render the same union everywhere, but two tabs editing the same field each keep showing their own value until a reconnect lets the server settle the order. The divergence is render-only — it changes nothing either tab journals or replays — and it is architectural rather than a bug: each tab is a full client with its own optimistic queue, and N queues over one base have no offline arbiter. Whether one should exist — a leader tab or SharedWorker owning a single queue — is tracked in open questions.