---
id: runtime/offline-persistence
title: Offline & Persistence
---

Packs are per-locale+namespace compiled blobs (`Uint8Array`), kept in an in-memory cache and optionally an async `PersisterAdapter` so they survive reloads without the network.

## Persister

```ts
import { createBestEffortPersister } from '@secundus-studio/stift-persisters'

export const persister = await createBestEffortPersister()
// OPFS → IndexedDB → Cache Storage → memory: first supported wins, never throws.
```

Pin one backend instead of the chain:

| Import | Backend | Notes |
|---|---|---|
| `@secundus-studio/stift-persisters/opfs` | Origin Private FS | Fastest; needs secure context |
| `@secundus-studio/stift-persisters/idb-keyval` | IndexedDB | Broad fallback |
| `@secundus-studio/stift-persisters/cache-api` | Cache Storage | Shares the browser cache quota |

All implement `PersisterAdapter = { get, set, del, keys }` over `Uint8Array` — custom backends implement the same interface directly, no registration needed. Wire via `createStift({ persister })`. With no persister, packs are memory-only (re-fetched each boot). `Stift.pack.durability` reports `'durable' | 'memory' | 'none'` — gate install/wipe UI on it: a memory persister looks healthy but evaporates on reload.

## Install → activate lifecycle

```ts
await Stift.installLocale('ar') // fetch + cache the pack(s); returns boolean
await Stift.activateLocale('ar') // load into memory + make it the active locale
Stift.isLocaleInstalled('ar') // → boolean
const size = await Stift.getPackSize('ar') // PackSizeInfo: byte budget vs. estimate
```

`activateLocale` is the reactive switch behind `useLocale().setLocale` for offline-resident locales. Use `getPackSize` to warn before installing a large pack on metered storage. Wipes are dialect-safe (wiping `ar` never wipes `ar-SY`): [Dialects](../authoring/dialects.md).

## Boot offline-first

```ts
const locale = await Stift.detectLocale() // custom (saved) > navigator > fallback
Stift.setLocale(locale)
void Stift.initialize({ locale }) // unawaited: first paint stays off the network
void Stift.pack.checkForUpdates(locale) // revalidate in the background
```

`initialize` fast-paths returning visitors: installed packs verify + activate from the persister with zero network, and the reactive `packInstalls` status rehydrates from disk so it reads `installed` from the first notification. Strategy choice: [Loading Strategies](../configuration/loading-strategies.md).

[//]: # 'PWA'

Packs never need a service worker. Locale packs, detection, and catalog/update checks all work without one. A SW only buys app-shell precaching (a fully offline *reload* of the shell); if you ship one, activate new versions with `skipWaiting` + `clients.claim()` so refreshes pull freshly-published packs without a hard reload.

[//]: # 'PWA'

## Wiring checklist

- **Suspense boundary**: components that suspend on a namespace load (`useTranslation`) must sit under a `<Suspense>` boundary, or the first render before anything is cached drops the tree. Boot modes: [Setup & Boot Modes](../react/setup.md).
- **Error surfacing**: wire `hooks.onError` to `console.error` (or inline UI) so failed pack downloads show the real error instead of failing silently.
- **Reset**: `Stift.pack.wipePersisted({ except: locale })` clears all downloaded packs except the active locale.
- OPFS requires a secure context (localhost fine; prod needs HTTPS); if unavailable, fall back to IndexedDB.

## Next

- Detecting changed packs with one ping → [Updates & Invalidation](./updates-invalidation.md)
- Pack state in React (`usePackStatus`) → [Setup & Boot Modes](../react/setup.md)
- Watching it all happen → [Devtools](../tooling/devtools.md)
