Skip to content

Offline readiness ​

app.offline answers one question: if this device reloaded with no network right now, would the app open, show the data it was told to keep, and accept writes? Design: docs/design/offline-readiness.md in the repository.

typescript
const status = app.offline.status()            // synchronous snapshot, never prompts, never throws
const stop = app.offline.subscribe((s) => {})  // called on every change of the snapshot

await app.offline.acknowledgeEviction()        // after telling the user a database vanished
app.storage.evicted                            // the unacknowledged storage loss, or null

React: const offline = useOfflineReady() (@korajs/react). Vue: const offline = useOfflineReady() returns a readonly ref (@korajs/vue). Svelte: const offline = createOfflineReadyStore(), also exported as useOfflineReady (@korajs/svelte). Each returns the same OfflineStatus and re-renders only when it changes. On the server it reports not-ready with server-rendering.

OfflineStatus ​

FieldMeaning
stateready, preparing (every missing piece is making progress) or not-ready
reasonsWhy not: empty exactly when state is ready
shellThe offline app shell: { state, version, misses }, see The app shell
datacomplete, syncing, incomplete or local; completedAt; quarantined
filesKept-file counts (zeros until files are kept)
storagebackend, durable, persisted, writes, evicted
authn/a, loading, restorable (with offlineUntil), not-restorable or unknown

unknown is never ready. persisted: false, quarantined operations and an unacknowledged eviction are reported but never change state.

The app shell ​

shell.state says whether the interface itself (HTML, scripts, styles, the SQLite WASM) opens with no network. Only installed and bundled count toward ready.

StateMeaningReason when it is not ready
installedThe active service worker holds every precached filenone
bundledA desktop build (native storage backend) embeds its interface in the binarynone
installingThe worker is registering or installing (first visit)shell-installing (progress, so preparing)
not-installedInstall failed, or the cache is incomplete and could not be healedshell-not-installed
other-buildThe page runs a build the active worker does not holdshell-other-build (resolved by accepting the update)
unsupportedNo service workers, or an insecure contextshell-unsupported
noneNothing reports a shell: no koraServiceWorker(), vite dev, or an older @korajs/clishell-none

shell.version is the active worker's precache version and shell.misses the URLs it could not serve offline (a font CDN shows up here); misses is diagnostic and never changes state.

The status comes from the worker that koraServiceWorker() (@korajs/cli/vite) generates, not from navigator.serviceWorker.controller, which is null after a hard reload. The worker verifies its cache and, when online, refetches a missing file and stores it only if its SHA-256 equals the digest recorded at build time; bytes of a newer deployment are never stored. The registration script publishes the result as a versioned contract that app.offline reads:

typescript
globalThis.__KORA_SHELL__ = {
  v: 1,                                    // contract version; another value reads as absent
  state: 'installed',                      // 'installing' | 'installed' | 'not-installed' | 'other-build' | 'unsupported'
  version: '3f9c1a7b2d4e5f60',             // the active worker's precache version, or null
  misses: ['https://fonts.example/css'],   // diagnostic, bounded
}
// and a `kora:shell` event on window each time it changes

See Opening the App Offline for the protocol. Every template renders an <OfflineBadge /> built on useOfflineReady().

What "complete" means ​

data.state is complete only when a durable completion mark proves the current view was fully delivered: the mark is written in the same transaction as the delivery watermark of the final batch, cleared before a batch that narrows the view or removes records, and cleared by every watermark reset. A delivery that crashed or stalled part-way leaves the view incomplete after a reload, never complete. "Complete" means complete as of data.completedAt; data the server received after the last sync is not here, which no device can know offline.

The marks are read when the sync engine starts (sync.autoConnect, or await app.sync.connect()).

Storage loss ​

When a database that existed on this device is reopened empty, Kora emits store:storage-evicted and keeps app.storage.evicted ({ dbName, detectedAt, lastOpenedAt, evidence, lostWrites }) until app.offline.acknowledgeEviction(). See the changeset notes for when it is not reported.

Sign-in ​

An app that signs users in is ready only when a reload with no network would restore the session. createKoraAuthSync (@korajs/auth) answers from the stored credentials:

Stored credentialsauth.state
Both tokens in localStorage (the default), refresh token not expired, inside maxOfflineGraceMsrestorable, offlineUntil is the earlier of the grace end and the refresh expiry
In memory or sessionStoragenot-restorable (non-durable-credentials)
A custom store that does not declare durabilityunknown, never ready
Signed out, locked, expired, or a clock set backnot-restorable

An app with its own token store sets durability: 'durable' on it (a platform keychain, the Tauri store). An app whose sign-in Kora cannot see (a bare sync.auth token function) declares it:

typescript
createApp({
  schema,
  sync: { url, auth: getToken },
  offline: { auth: () => ({ restorable: hasStoredSession(), until: sessionLimit() }) },
})

ready flips off by itself when offlineUntil passes, and when the device clock moves backwards (checked every 30 seconds while the session is restorable, and on page resume and online/offline events). Local data is never wiped when a session lapses.