Skip to content

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.

DomainOffline behavior
Spaces messagesHistory + live messages are cached. space.cachedMessages() hydrates instantly on boot. Messages sent offline are applied optimistically and queued, then replayed on reconnect.
client.kvCache-first reads, optimistic writes, durable queue, realtime-feed merge.
client.dbget/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 stateclient.offline.snapshot — an encrypted local cache for UI state.

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.

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 cache
for (const m of await space.cachedMessages()) render(m); // instant, works offline
await space.connect(); // then go live + catch up
space.onMessage(render);

When the connection returns, the Space pages a forward delta from your last-seen message and merges anything you missed.

Concurrent edits converge deterministically using Hybrid Logical Clocks — no lost updates, no “last save wins” surprises:

  • client.kv values 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.db rows 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).

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/absent

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.