Offline Mode
Every plasma app is offline-first in the sense that mutations return immediately from IDB and the sync loop happens in the background. But plasma has two additional knobs when you want more offline:
PlasmaClientOptions.offline: true— skip the network entirely. Useful for React Native, Tauri, Electron, or air-gapped environments where there’s no plasma sync server at all.TableOptions.changeLogSuppressed: true— per-table opt-out of the sync loop. Local-only cache tables that never leave the browser.
They compose freely.
offline: true — client-wide
Section titled “offline: true — client-wide”const plasma = createPlasmaClient({ schema, mutators, dbName: "notes", endpoint: "/sync", // still required (dummy value fine) clientGroupID: "user-42", schemaVersion: "notes-v1", getContext: async () => ({ userId: "u1" }),
offline: true,})When set:
client.start()does NOT open the poll timer /onlinelistener / WebSocket subscription.client.pushOnce()andclient.pullOnce()are no-ops (they resolve immediately without any HTTP call).- Blob uploads don’t run.
client.readFile(ref)serves from_plasma_blobs_localonly; uploads that would otherwise transition a blob fromlocal→readystaylocal. - The outbox still accumulates. Mutations are enqueued as
normal. When you flip
offlineback tofalse(by reconstructing the client), the outbox flushes through the normal push path.
The resetLocalState refusal
Section titled “The resetLocalState refusal”client.resetLocalState() in offline mode throws:
plasma: resetLocalState() is not supported when PlasmaClientOptions.offline is true — the local state cannot be re-hydrated from the server and the offline outbox would be lost.
An offline app has no server to re-hydrate from. Wiping local state would destroy every un-flushed mutation without any way back. If you truly need it, flip to online first, reconstruct the client, and reset there.
changeLogSuppressed — per-table
Section titled “changeLogSuppressed — per-table”Some tables belong to the client only. Session drafts, per-tab caches, ephemeral filter state that should survive a reload but should never sync to the server:
const drafts = table("drafts", { id: id(), snapshot: text(), updatedAt: int(),}, { changeLogSuppressed: true,})
const schema = defineSchema({ todos, // synced (default) drafts, // local-only})Effects on the server side:
ensureSchema/runMigrationsskip the AFTER-write triggers on suppressed tables. Raw driver writes todraftsdon’t produce_plasma_changesrows.- Pull responses never include changes for suppressed tables.
Effects on the client side:
rebuildOptimisticskips suppressed tables when it clears the user-visible store. Local-only rows are preserved across pulls.runMutateinspects which tables the mutator touched (via the newengine.recordTouchedTableshook). If every touched table is suppressed, the outbox entry is skipped entirely — no push ever goes out for that mutation.
Composing offline + changeLogSuppressed
Section titled “Composing offline + changeLogSuppressed”They work together. A Tauri app might use offline: true on the
whole client, then use changeLogSuppressed: true on a few tables
for “even the internal change log is unnecessary bookkeeping”:
- Every write is a local IDB write.
- Non-suppressed tables still populate the
_plasma_changestriggers (in case you ever want to flip online later). - Suppressed tables skip the triggers too — pure local state.
When the network comes back
Section titled “When the network comes back”PlasmaClientOptions.offline is a construction-time flag. There’s
no client.setOffline(true) runtime toggle in v1.0.
To transition an app from offline to online:
- Save any un-flushed state that the offline outbox is holding (usually nothing — plasma will flush it for you).
- Reconstruct the client with
offline: false. - Call
client.start().
The IDB store (base + user stores + outbox) is shared across
reconstructions because it’s keyed by dbName. Every outbox entry
from the offline run flushes through on the first push.
Detecting offline vs online
Section titled “Detecting offline vs online”The client fires SyncClientError events with kind: "network"
when a fetch fails. In practice you can subscribe by adding an
onError:
createPlasmaClient({ ..., onError: (err) => { if (err.kind === "network") { setBanner("offline") } },})
// Reset banner on next successful push/pull:There’s no built-in “online now” event — plasma retries transparently.
Reach for navigator.onLine if you need to react to browser-level
online/offline transitions.
What to read next
Section titled “What to read next”- Concepts / Push, Pull, Rebase —
what the sync loop skips when
offline: true - Concepts / Change Log and Cookies
— the triggers
changeLogSuppresseddisables - Troubleshooting / Sync errors — triaging offline-mode failures