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.
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.
- A value with
fromCache: truecame from the device, not this request's network round trip. Anot_foundfor a key the device already knows is gone carries the same tag. See Reads offline for exactly which reads answer this way, and what an offlinelist/getBatchpage can and can't guarantee. - A write result with
pending: trueis queued, not yet acknowledged by the server. See Writes offline for how each write method behaves offline, and what "queued in order, never overtaken" means for a key you're writing more than once.
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:
auth.me()answers with the last identity this device confirmed, instead ofnull, when the network is down — so a gated UI (a "welcome back" header, an admin-only control) doesn't flicker to signed-out every time the connection drops.auth.logout()gives queued writes a moment to send, then holds — never discards — whatever didn't make it, for that same account, rather than losing it. Nothing in your logout code needs to change; the behavior is automatic.
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:
- Load the deployed glow online first, so the device has something cached and (if you're signed in) an identity to fall back on.
- Turn the network off — airplane mode on a phone, or your browser devtools' network-throttling panel set to "Offline" on desktop.
- 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. - Make a write. It should resolve immediately with
pending: true— checkoffline.pending()and you should see it listed. - 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 calloffline.sync()yourself if you'd rather not wait. Either wayoffline.status().pendingshould 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.