Offline
Muhkoo apps work offline with no extra code. The SDK caches every data domain
locally, lets the user keep reading and writing while disconnected, and
reconciles automatically when the connection returns — all behind
client.offline.
It’s on by default in browsers (where IndexedDB and the Cache API exist) and a no-op everywhere else (Node, Workers, SSR), so existing server-side code is unaffected.
What works offline
Section titled “What works offline”| Domain | Offline behavior |
|---|---|
| Spaces messages | History + live messages are cached. space.cachedMessages() hydrates instantly on boot. Messages sent offline are applied optimistically and queued, then replayed on reconnect. |
client.kv | Cache-first reads, optimistic writes, durable queue, realtime-feed merge. |
client.db | get/query cached for offline reads; writes optimistic + queued + replayed. |
client.storage (files) | Shard bytes cached (content-addressed); offline uploads queued. Reads recover from cached shards. |
| App state | client.offline.snapshot — an encrypted local cache for UI state. |
Reacting to connectivity
Section titled “Reacting to connectivity”client.offline.status is "online", "offline", or "syncing" (draining the
queue after a reconnect). Subscribe to render a status indicator:
const off = client.offline.onStatusChange((status) => { banner.hidden = status === "online"; banner.textContent = status === "offline" ? "Offline — changes will sync later" : "Syncing…";});The SDK fuses navigator.onLine, real request outcomes, and the websocket
lifecycle, so it’s more reliable than the browser’s online/offline events
alone.
Offline-first chat hydrate
Section titled “Offline-first chat hydrate”Paint a Space’s saved backlog immediately, before the network is even available:
const space = await client.space.get(spaceId);await space.keyring.loadFromCache(); // group key from the offline kv cachefor (const m of await space.cachedMessages()) render(m); // instant, works offlineawait space.connect(); // then go live + catch upspace.onMessage(render);When the connection returns, the Space pages a forward delta from your last-seen message and merges anything you missed.
Conflict resolution (CRDTs)
Section titled “Conflict resolution (CRDTs)”Concurrent edits converge deterministically using Hybrid Logical Clocks — no lost updates, no “last save wins” surprises:
client.kvvalues are an LWW-Register: the write with the greater clock wins; deletes are causal tombstones (a stale write can’t resurrect a deleted key).client.dbrows are a per-column LWW-Map: two devices editing different columns of the same row while offline both keep their edits; same-column conflicts resolve by clock. Deletes are causal tombstones.- Messages are an append-only log ordered by a server-assigned handle; edits resolve by clock, deletes tombstone.
- Files are content-addressed and immutable, so they’re convergent by construction.
Convergence for kv and db is honored server-side too — the server merges by
clock rather than blindly overwriting (it stays blind to encrypted kv values).
App-state snapshots
Section titled “App-state snapshots”Stash arbitrary view state to repaint instantly on a cold (even offline) boot — the last open Space, a draft, scroll position. Encrypted at rest with the user’s identity key, local-only by default.
await client.offline.snapshot.save("ui", { route, openSpaceId, draft });const ui = await client.offline.snapshot.load<UiState>("ui"); // null when locked/absentTurning it off
Section titled “Turning it off”Offline support auto-enables in browsers. Override via the client options:
new Client({ offline: { enabled: false, // force off (or true to force on) cacheShards: true, // cache file-shard bytes (default true in browser) maxQueueBytes: 5_000_000, // store: myOfflineStore // supply a custom OfflineStore },});See the client.offline reference for the full API.