# 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`](https://sdk.backlit.run/sdk.md#backlitoffline--offline-data)
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](https://sdk.backlit.run/guides/offline-install.md)
(`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`:

```js
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()`](https://sdk.backlit.run/sdk.md#offlineenableopts-offlineoptions-promiseofflinestatus)
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()`](https://sdk.backlit.run/sdk.md#backlitoffline--offline-data)
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: true` came from the device, not this request's
  network round trip. A `not_found` for a key the device already knows is
  gone carries the same tag. See
  [Reads offline](https://sdk.backlit.run/sdk.md#reads-offline) for exactly
  which reads answer this way, and what an offline `list` / `getBatch` page
  can and can't guarantee.
- A write result with `pending: true` is queued, not yet acknowledged by the
  server. See
  [Writes offline](https://sdk.backlit.run/sdk.md#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`](https://sdk.backlit.run/sdk.md#offlinewatchcb-unsubscribe).

## 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](https://sdk.backlit.run/sdk.md#device-view-reads-offlinepeek-and-offlinecached).

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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#waiting-for-sync-offlinewhensynced).

## 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](https://sdk.backlit.run/sdk.md#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
  of `null`, 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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/guides/offline-install.md)
(`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](https://sdk.backlit.run/sdk.md#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.
