Offline data (backlit.offline)

A glow's data calls normally need the network — that's the whole point of backlit.data / userdata / records: they talk to a server. backlit.offline is the opt-in that changes that: once you call it, reads fall back to a copy on the device when the network is down, and writes queue up and send once it comes back. Nothing changes until you ask for it, and the full contract — every method, tag, default and limit — lives in the reference's backlit.offline section. This guide is the story around it: when to turn it on, how to build UI that tells the truth about what's cached and what's queued, what changes for the signed-in user, and how to actually go test it.

This is a different feature from offline install (offline.json), which caches your app's files so it launches with no network at all. The two are independent and pair naturally — more on that below.

Plain text All guides SDK reference

Turn it on, once, at startup

Call enable() once, as early as the page allows — module load or DOMContentLoaded:

const status = await backlit.offline?.enable();

You don't have to sequence the rest of your start-up around it. Data calls made while enable() is still in flight wait for it automatically (briefly), so enable(); data.get(k) is fine. If you want to know when it has finished — to show the first "you're offline" indicator, say — offline.whenReady() resolves the status; see the reference's enable() entry for the exact rules.

The ?. matters more than it looks: it's what keeps an older cached copy of the SDK bundle (from before this namespace existed) from throwing on this line instead of just not having offline data. You don't have to guard against calling enable() more than once, or coordinate it with other code that might also call it — see the reference's enable() entry for exactly what happens on a second call.

Still, don't assume it worked. enable() resolves a status rather than throwing when offline data isn't available in this context — an unsupported browser, a host that isn't a deployed glow, on-device storage that exists but won't open. Check status.enabled and, if it's false, status.reason for why; the full outcome list and every option enable() takes (which stores, how writes behave, onConflict, maxBytes, and the rest) are in the reference section linked above — this guide won't restate them.

Design around fromCache and pending, don't hide them

Once offline data is on, a read can answer from the device instead of the network, and a write can resolve before it has actually reached the server. Both cases are tagged, and the tag is the whole point: it's your cue to tell the visitor "you're offline" or "saved — syncing" rather than presenting a cached or queued result as if it were confirmed.

offline.status() gives you the aggregate picture (how many writes are pending, whether the queue is currently syncing, whether this session is too stale to answer from the cache) and offline.watch() pushes you individual events — a write queued or sent, a conflict, the on-device copy being purged — as they happen. Reach for status() for a persistent indicator (a banner, a badge) and watch() for one-off toasts. watch() gets a fixed default set of events; to hear about newer ones (the device dropping values to make room, say) name them in its events option — see offline.watch.

Paint from the device, then refresh

A normal read waits for the network before it falls back. To put something on screen the instant the page opens, read the device's copy first with offline.peek() / peekJSON() — no request is made — then refresh with the normal read. offline.cached() describes what the device holds under a prefix, and pages answered from the device carry complete so you can tell "nothing more" from "nothing more here". Details and the identity rule are in Device-view reads.

If you know a visitor is about to go offline — a field visit, a flight — offline.prefetch() fetches a prefix onto the device ahead of time, and pinning keeps it there. Prefetching spends real requests, so bound it; see Prefetch and pins for the cost, the limits and what a pin does and doesn't guarantee. For a "saved" indicator, offline.whenSynced() resolves once the write queue has nothing more it can send — see Waiting for sync.

Handle conflicts, or let the default carry you

A queued write can meet a newer value once it finally reaches the server — someone else, or another tab, wrote the same key while you were offline. Do nothing and the server's value wins: the write is dropped and an event fires so you can refresh what's on screen. If your app needs to merge instead of losing the local edit, pass onConflict to enable(). If the write went through data.updateJSON (or its userdata / records equivalents), you often don't need to write any conflict code at all — the same mutate function you already passed in reruns automatically against the newer value. The full resolution contract — what onConflict receives, what it can return, and the remote-wins default's timing — is in Conflicts.

What changes for the signed-in user

Two calls you already use behave differently, and only while offline data is on:

The exact rules — how long a confirmed identity stays usable offline, and what happens to held writes if a different user signs in on the same device — are in Identity.

Pair it with offline.json for a true cold launch

backlit.offline keeps your app's data working once the app is already open and running. It doesn't get the app's HTML, JS and CSS onto the device in the first place — that's a separate, independent opt-in, offline install (offline.json). Ship only backlit.offline and a visitor who has never loaded your app online can't launch it at all; ship only offline.json and the app opens fine offline but every data call still fails. Ship both, and an app that's been opened online once can be relaunched with no network, data included. If your app is meant to be usable offline from a cold start — not just resilient to a mid-session network drop — read the offline install guide next.

Test it before you ship it

The fastest way to find out whether your offline handling actually works is to go offline on purpose:

  1. Load the deployed glow online first, so the device has something cached and (if you're signed in) an identity to fall back on.
  2. Turn the network off — airplane mode on a phone, or your browser devtools' network-throttling panel set to "Offline" on desktop.
  3. Reload. The app should still open, and a read you've already made should answer from the cache (fromCache: true) instead of hanging or erroring.
  4. Make a write. It should resolve immediately with pending: true — check offline.pending() and you should see it listed.
  5. Turn the network back on. The queue drains on its own: the page checks for the connection while it's offline and visible, notices it's back and sends what's queued (how, and how to tune it, is under Reachability and readiness). Await offline.whenSynced() to know when it's done, or call offline.sync() yourself if you'd rather not wait. Either way offline.status().pending should drop to zero and the write should show up wherever it's meant to render, now unqueued.

Do this once for every store you enabled and once for a write that deliberately collides with a change made from another tab (or another browser), so you see your conflict handling — or the remote-wins default — fire for real before a visitor finds it for you.