Durability
What browsers actually promise about storage, said plainly.
Browser storage is not automatically permanent. Every value this package writes lives inside a set of policy decisions the browser controls: LRU eviction, ITP timeouts, private-mode ephemerality, quota limits.
The library's promise is:
The package commits state correctly to browser storage and exposes the browser's durability status.
The library does not promise:
Your state can never disappear.
State may disappear because of:
- explicit site-data clearing
- browser profile loss
- device loss
- private-browsing lifecycle
- browser eviction of best-effort storage
The status object
durable.storage() returns an honest snapshot:
{
persistent: boolean, // navigator.storage.persisted()
quotaBytes: number | null, // navigator.storage.estimate().quota
usageBytes: number | null, // navigator.storage.estimate().usage
quotaRounded: boolean, // true on WebKit + private modes
privateMode: "unknown" | "likely" | "no",
evictionRisk: "session" | "ephemeral" | "best-effort" | "persistent" | "ios-capped",
engine: "chromium" | "gecko" | "webkit" | "unknown",
installedWebApp: boolean,
}evictionRisk is the field to branch UI on. Especially:
ios-capped— this is Safari's specific trap. Even whennavigator.storage.persist()resolvestrue, ITP still evicts a regular Safari tab's data after 7 days without user interaction. The only way out is for the user to install the site as a Home Screen web app; then their interaction with the app resets the timer. Surface this to the user; do not paper over it.
Requesting persistence
const granted = await durable.requestPersistence();Chromium auto-decides silently based on engagement. Firefox shows a UI prompt. Safari uses heuristics that favor installed web apps. Do not call this at page load; call it when the user has done something that proves the value has become worth protecting — the first significant write, a "save" affordance, a settings change. Asking too early gets denied and the browser remembers the denial.
Quota
If a commit would exceed the origin's quota, IndexedDB aborts the
transaction with QuotaExceededError. The library re-throws as
QUOTA_EXCEEDED. The previous committed value remains authoritative.
Subscribers do not see the failed value.
Private browsing
Detected heuristically via quota size. If privateMode is "likely"
your storage may vanish when the private tab closes; treat every commit
as belonging to that session only. Chrome Incognito, Firefox Private
Windows, and Safari Private all fall into this bucket.
What to do with all of this
At startup:
- Call
durable.storage(). - If
evictionRisk === "session", tell the user their work will not persist beyond this window. - If
evictionRisk === "ios-capped", offer the "install as web app" nudge. - If quota is very low (
quotaBytes < 50 MB), warn before allowing heavy edits.
At write time, do not do any of this — just call the API and let it succeed or throw. Chatter at startup is the honest place; chatter at write time is UX noise.