Offline install (offline.json)
Every glow is already installable to a home screen. Shipping one small file
at the root of your deploy bundle is what makes the installed app work
with no network: it launches, paints its shell, and runs your code from a
copy the device already holds. Nothing else about the app changes — same
bundle, same window.backlit.* calls, same auth mode. The file is
offline.json, and its presence is the whole opt-in.
This guide is the story of that file: what the two caching tiers buy you, how to choose what gets installed, what clears the copy, and what still needs the network. The canonical contract is the reference's Offline install section, and every field, default and constraint lives in the offline.json schema. This guide links to both rather than repeating them, so it can't drift out of sync with them.
The two tiers
One file gives you two layers of caching, and you get both.
Tier 1 — the shell you have already seen. As a visitor browses your app online, the pages and assets their browser loads are kept. Come back with no network and the app launches from that copy instead of failing. The cost is one online visit for each part of the app you want available later.
Tier 2 — the precache. When a device notices a new live version, it downloads the files you nominated up front, before anyone asks for them. The whole app is then available offline immediately after the install — not just the corner of it someone happened to open.
Both tiers come from the same offline.json; there is no second switch.
Ship the file and Tier 1 starts working on the first visit, while the
precache list decides how much of your app Tier 2 pulls down. The
reference's
Offline install
section is the authority on what each tier guarantees.
Turn it on
offline.json rides your deploy bundle as an optional file at the archive
root, alongside index.html and the contract documents a bundle already
carries — see
Deploy bundle contract
for what must be in a bundle and
Packaging the deploy bundle
for how to assemble and upload one.
The smallest valid file is the empty object:
{}
That is a complete offline.json. It means "offline on, precache
everything", so you only reach for the fields below when you want less than
everything — the
offline.json schema
is the authority on what each field defaults to.
Offline state travels with the version that carried the file, so the app becomes offline-capable when that version goes live, and switches back off when a version without the file does (rolling back counts). The reference's Offline install section spells out that lifecycle.
Choose what is precached
Two optional lists shape the precache set: precache names what goes in,
exclude takes things back out. Both are lists of bundle-relative glob
patterns, and both are defined — dialect, limits and defaults — in
the offline.json schema.
{
"precache": ["index.html", "app.js", "styles/**", "icons/*.svg"],
"exclude": ["styles/print-*.css"],
"navigation_fallback": "index.html"
}
(A $schema key naming
the schema is
accepted too, so your editor can offer completion and inline validation as
you write the file.)
Read as narrative: install the entry document, the app script, everything
under styles/ at any depth, and the SVGs sitting directly in icons/;
then drop the print stylesheets from whatever that selected; and, offline,
serve index.html for a page the device has no cached response for.
The glob dialect is deliberately small: * matches any run of characters
within one path segment, ? matches exactly one character, and a segment
of ** matches whole segments. Patterns are also written from an
allow-listed character set — [A-Za-z0-9._~*?-] within a segment, with
/ separating segments. Both rules, and the
exact behaviour of ** in each position, are pinned by
the schema. One
consequence is worth knowing up front: a bundle path may legally contain
characters outside that set — a space, a bracket — so such a file can be
matched by a wildcard but never spelled literally. Rename it if you
need to name it.
Some entries are not yours to decide. The bundle's contract documents and
the platform's own config files — APP.md, DATA.md, notify.json and
offline.json itself — are always excluded and cannot be re-added, while
index.html is always included last and cannot be excluded;
the schema carries the
complete always-excluded set. And a file matched by no pattern is simply not
precached — never an error.
There is a cap on how many files a precache set may hold and a cap on their
total size. Go over either and the deploy is rejected rather than
silently trimmed: the
deploy API reports
offline_too_large, and a malformed offline.json — or one reaching for
something the platform disallows — is rejected as invalid_offline. Both
land at upload, so you learn about them at deploy time rather than on a
user's phone.
Navigation fallback
navigation_fallback is optional and off by default. Name a page and an
offline navigation the device has no cached response for is served that page
instead of failing — the single-page-app shape, where one document routes
everything client-side. Leave it out and an uncached offline navigation
fails, exactly as it would without offline at all.
The schema is the
authority on which values the field accepts.
The one rule to hold on to: whatever you nominate must itself end up in
the precache set. Exclude it and the deploy is rejected with
invalid_offline (see the
deploy API) — a fallback the device
never downloaded could never be served, so the platform refuses the bundle
rather than shipping a promise it can't keep.
What happens on sign-out and when a session is rejected
Offline works on every auth mode — Public, Private and Accounts alike; see Public, Private, and Accounts glows for what each mode admits. What differs between them is when the cached copy is thrown away.
The copy is dropped when:
- a user signs out of a Private glow. The sign-out that ends the session
(
backlit.auth) also clears the device's copy of the app, so the next person to pick up that phone starts from nothing. On a Public or Accounts glow there is nothing user-specific to clear, so signing out leaves the shell in place. - a session stops being accepted. An expired session, or a user removed from a glow's allowlist, sends the next online visit to sign-in instead of the app — and that answer clears the copy too.
- you switch the app off. Remove
offline.jsonand deploy again; once a device picks that up, it clears what it had stored.
After any of those the next successful online visit rebuilds the cache from
scratch. One honest caveat: those are all things the device only learns
online. Between a revocation and the device's next successful online
visit, an already-cached shell can still launch — it renders empty, because
offline install itself never caches store data (see below; an app that
separately opted into
backlit.offline
may still have something to show from that copy instead) — but the shell
itself is there.
What offline does not cover
Offline install caches your app's files, not its data. Shipping
offline.json alone doesn't change what
backlit.data
or its userdata / records / capture siblings do once the network is
down — those calls still need it, and fail like any other request that
can't reach the server. Design for that: paint the shell first, then fill it
in when a read succeeds.
If you also want your app's data to keep working offline — reads answered
from a copy on the device, writes queued and sent once the connection
returns — that's a second, independent opt-in:
backlit.offline.
Ship this file, that call, or both; neither implies the other. See the
offline data guide for how
to turn it on and design around it.
Five more things worth knowing before you ship:
- Online, you always get the newest deploy. Navigations go to the network first, so a new version is picked up on the next load; the cached copy is only ever used when the network is unavailable.
- An app launched from the cached copy can tell it is behind. When the
device reconnects and a newer deploy is live, the SDK fires
"app-update-available"; listen for it and offer a reload. Seebacklit.checkVersion(). - Precaching is a real download, per device, per version. Each device
fetches the set once for every version it sees, and those requests are
metered like any other request to your glow. A large precache list on a
busy glow is a real cost — that is precisely what
excludeis for. window.backlit.*still exists offline. The SDK bundle is cached alongside your files, so the surface is there to call (and to fail honestly) instead of the page dying on a missing script. See Loading for how the bundle gets onto the page in the first place.- Browsers may evict what they cached. iOS and Safari in particular can clear a site's storage after a stretch of disuse, so treat a long-dormant app as possibly cold — its next launch may need the network again.
Ship it with an agent
You don't have to package and upload by hand. An AI assistant connected to
the hosted MCP server can create the glow,
deploy the bundle — offline.json included — as a draft, and promote it
live once you've previewed it. See
Deploy with an agent
for the connection details and the out-of-band deploy transports.
Going further: offline.json has a sibling optional bundle file,
notify.json, covered by the
push guide — the two
pair naturally, since a doorbell is most useful on an app that launches
instantly. If you're still getting to a first deploy, the
migration guide
and the greenfield guide
walk that path end to end.