pwa
@noy-db/in-pwa·pnpm add @noy-db/hub @noy-db/in-pwaBrowser-shell plumbing for an installed noy-db app.
The premise: the PWA starts empty
An installed PWA lives in its own storage partition. On first open it holds nothing: the app runs the online enrollment (unlock + re-invite via on-oidc) and hydrates the vault from the firm's cloud store into to-browser-idb. From then on the installed app is the offline-capable home of the vault — offline boot is a cached app shell plus the local IndexedDB vault.
This package is the plumbing around that lifecycle. It holds no keys and sees no plaintext: every helper operates on browser shell APIs or on opaque ciphertext-store presence, so it adds nothing to the threat model.
Eviction is the top risk
iOS ITP evicts all script-writable storage — IndexedDB included — of non-installed web content
after about seven days without interaction. Installed home-screen apps are safer, and
navigator.storage.persist() exempts an origin from best-effort eviction where granted. Two
helpers cover it.
requestPersistence() — ask, and know the answer
import { requestPersistence } from '@noy-db/in-pwa'
const res = await requestPersistence()
// { persisted: boolean, quota?: number, usage?: number,
// grantedBy: 'already' | 'granted' | 'denied' | 'unsupported' }
Wraps persist() + estimate() with an explicit grant/deny signal, and never throws —
browsers without the Storage API resolve to grantedBy: 'unsupported'. Call it during enrollment
and warn the user on 'denied'.
guardLocalVault() — fail closed at boot
If the partition was evicted, the app must never crash and never present a silent empty vault as truth. The guard probes the store before the vault is opened and routes a miss into re-enrollment:
import { guardLocalVault } from '@noy-db/in-pwa'
import { toBrowserIdb } from '@noy-db/to-browser-idb'
const store = toBrowserIdb()
const { healthy } = await guardLocalVault(store, 'firm', (why) => {
// why: { present: false, reason: 'empty' | 'probe-failed', cause? }
routeToReEnrollment()
})
if (healthy) {
/* open the vault normally */
}
probeLocalVault(store, vaultId) is store-agnostic — it speaks only the six-method
NoydbStore contract, never to-browser-idb internals. It checks the
vault's _keyring marker records first (every encrypted vault persists its owner keyring at
creation), then falls back to a loadAll() envelope scan for plaintext-mode vaults.
A broken store is treated exactly like a missing one: probe errors resolve to
{ present: false, reason: 'probe-failed' } and trigger onEvicted. The probe path never throws.
Install UX
import { captureInstallPrompt, getDisplayContext, isIosSafari } from '@noy-db/in-pwa'
const install = captureInstallPrompt() // once at boot — defers the browser prompt
const outcome = await install.promptInstall() // later, on a user gesture:
// 'accepted' | 'dismissed' | 'unavailable'
getDisplayContext() // 'pwa' (standalone display-mode / iOS navigator.standalone) | 'browser'
isIosSafari() // true → show the "Add to Home Screen" interstitial;
// iOS never fires beforeinstallprompt
The shell-context contract shared with in-liff is exported here as a plain string union, with no runtime coupling in either direction:
export type AppShellContext = 'liff' | 'browser' | 'pwa'
Online/offline
import { watchOnline } from '@noy-db/in-pwa'
const stop = watchOnline((online) => syncStrategy.setOnline(online))
Fires immediately with navigator.onLine, then on every online/offline event — shaped for
feeding the sync engine's isOnline flag. Reconnect replay of the dirty queue
lives in the sync service; this package does not import it.
Service worker: a recipe, not a runtime
This package ships no service-worker code. The app-shell SW is a recipe you copy and version yourself — see the package README.
to-browser-idb; the worker caches the app shell only — HTML, JS, CSS, icons. Caching vault
responses would create a second, unmanaged copy of ciphertext outside the store contract, and a
plaintext copy if you cached decrypted API responses.See also
- in-* family
- liff — the other half of the portal shell
- to-browser-idb — where the local vault actually lives
- sync — the engine
watchOnlinefeeds