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.

Plain text All guides SDK reference

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 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:

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:

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.