# 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](https://sdk.backlit.run/sdk.md#offline-install-offlinejson)
section, and every field, default and constraint lives in
[the offline.json schema](https://sdk.backlit.run/schema/offline.schema.json).
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](https://sdk.backlit.run/sdk.md#offline-install-offlinejson)
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](https://sdk.backlit.run/sdk.md#deploy-bundle-contract)
for what must be in a bundle and
[Packaging the deploy bundle](https://sdk.backlit.run/sdk.md#packaging-the-deploy-bundle)
for how to assemble and upload one.

The smallest valid file is the empty object:

```json
{}
```

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](https://sdk.backlit.run/schema/offline.schema.json)
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](https://sdk.backlit.run/sdk.md#offline-install-offlinejson)
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](https://sdk.backlit.run/schema/offline.schema.json).

```json
{
  "precache": ["index.html", "app.js", "styles/**", "icons/*.svg"],
  "exclude": ["styles/print-*.css"],
  "navigation_fallback": "index.html"
}
```

(A `$schema` key naming
[the schema](https://sdk.backlit.run/schema/offline.schema.json) 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](https://sdk.backlit.run/schema/offline.schema.json). 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](https://sdk.backlit.run/schema/offline.schema.json) 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](https://glow.backlit.run/openapi.yaml) 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](https://sdk.backlit.run/schema/offline.schema.json) 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](https://glow.backlit.run/openapi.yaml)) — 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](https://sdk.backlit.run/sdk.md#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`](https://sdk.backlit.run/sdk.md#backlitauth)) 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.json` and 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`](https://sdk.backlit.run/sdk.md#backlitoffline--offline-data)
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`](https://sdk.backlit.run/sdk.md#backlitdata--glow-shared-keyvalue)
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`](https://sdk.backlit.run/sdk.md#backlitoffline--offline-data).
Ship this file, that call, or both; neither implies the other. See the
[offline data guide](https://sdk.backlit.run/guides/offline-data.md) 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. See
  [`backlit.checkVersion()`](https://sdk.backlit.run/sdk.md#backlitcheckversion--app-updates).
- **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 `exclude` is 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](https://sdk.backlit.run/sdk.md#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](https://mcp.backlit.run/mcp) 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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/guides/push-end-to-end.md) — 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](https://sdk.backlit.run/guides/migrate-a-static-app.md)
and the [greenfield guide](https://sdk.backlit.run/guides/build-a-new-app.md)
walk that path end to end.
