Skip to content

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.

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 / online listener / WebSocket subscription.
  • client.pushOnce() and client.pullOnce() are no-ops (they resolve immediately without any HTTP call).
  • Blob uploads don’t run. client.readFile(ref) serves from _plasma_blobs_local only; uploads that would otherwise transition a blob from localready stay local.
  • The outbox still accumulates. Mutations are enqueued as normal. When you flip offline back to false (by reconstructing the client), the outbox flushes through the normal push path.

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.

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 / runMigrations skip the AFTER-write triggers on suppressed tables. Raw driver writes to drafts don’t produce _plasma_changes rows.
  • Pull responses never include changes for suppressed tables.

Effects on the client side:

  • rebuildOptimistic skips suppressed tables when it clears the user-visible store. Local-only rows are preserved across pulls.
  • runMutate inspects which tables the mutator touched (via the new engine.recordTouchedTables hook). If every touched table is suppressed, the outbox entry is skipped entirely — no push ever goes out for that mutation.

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_changes triggers (in case you ever want to flip online later).
  • Suppressed tables skip the triggers too — pure local state.

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:

  1. Save any un-flushed state that the offline outbox is holding (usually nothing — plasma will flush it for you).
  2. Reconstruct the client with offline: false.
  3. 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.

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.