The iOS Safari 7-Day Storage Eviction Workaround
Users of your offline-capable web app on iPhone and iPad report that their data vanishes “after about a week” of not opening the app. They were not logged out by you, no error fired, and the same app behaves fine on Android. The cause is Safari’s Intelligent Tracking Prevention (ITP): on iOS and macOS, Safari deletes all script-writable storage — IndexedDB, the Cache API, localStorage, and sessionStorage — after 7 days of no interaction with the site, for any origin the user has not installed as an app. This guide explains the cap and gives concrete, runnable workarounds. For foundational context review Storage Quotas & Eviction Policies.
The specific problem
Safari’s privacy engine treats unattended script-writable storage as a tracking risk. The relevant rule, applied on iOS Safari 16 and 17 (and the matching macOS Safari builds), is a 7-day cap on script-writable storage for origins the user has not added to the Home Screen. If the user does not interact with your site for seven days, Safari clears IndexedDB databases, Cache API entries, localStorage, and sessionStorage for that origin. Cookies follow a separate, longer ITP schedule, which is why a server-backed recovery path survives even when local storage is gone.
The wipe is silent. No quotaexceeded, no error event — your next visit simply opens to empty databases. For a PWA whose value proposition is offline availability, that is a correctness bug, not a cosmetic one.
Root cause: the script-writable storage 7-day cap
The behavior comes from ITP’s classification of storage written by script. Anything a page writes through JavaScript — Web Storage, IndexedDB, the Cache API — is “script-writable” and subject to the cap. The only reliable exemptions are (1) the user installs the site to the Home Screen, which moves it into a durable context, and (2) the origin is granted persistent storage, which signals the data is user-valuable. Until one of those holds, treat local data as a 7-day cache, not a store of record.
The companion guide Debugging Storage Eviction in Progressive Web Apps covers how to confirm a wipe happened; the cross-browser picture is in Browser Storage Limits Across Chrome, Firefox, and Safari.
Step-by-step workarounds
Step 1 — Request persistent storage
navigator.storage.persist() asks the browser to exempt your origin from eviction. On Safari the grant is conservative, but requesting it is harmless and helps where supported. Check the result with persisted().
// Request durable storage and report whether the browser granted it.
async function requestPersistence(): Promise<boolean> {
if (!navigator.storage?.persist) {
console.warn('Storage Manager API unavailable — assume eviction applies');
return false;
}
const already = await navigator.storage.persisted();
if (already) return true;
const granted = await navigator.storage.persist();
console.info(`navigator.storage.persist() granted: ${granted}`);
return granted;
}
Step 2 — Prompt Add to Home Screen
An installed PWA runs in a durable storage context that is exempt from the 7-day cap on iOS Safari 16 and 17. Safari does not fire beforeinstallprompt, so you detect standalone mode and show your own hint when the app is running in a tab.
// True when the app is already installed (standalone display mode).
function isInstalled(): boolean {
const standalone = window.matchMedia('(display-mode: standalone)').matches;
// iOS Safari exposes a non-standard navigator.standalone for installed PWAs.
const iosStandalone = (navigator as unknown as { standalone?: boolean }).standalone === true;
return standalone || iosStandalone;
}
// Detect iOS and surface an Add-to-Home-Screen tip if not installed.
function maybePromptInstall(showTip: (msg: string) => void): void {
const ua = navigator.userAgent;
const isIos = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIos && !isInstalled()) {
showTip('Add this app to your Home Screen to keep your offline data from being cleared.');
}
}
Step 3 — Keep recoverable state server-side
Design so a wipe costs one network round-trip, not a re-login. Store the durable source of truth on the server, keep only a derived cache locally, and re-hydrate on cold start when the cache is empty. Because the refresh cookie follows the longer ITP schedule, the recovery request can authenticate even after local storage is gone — the same in-memory-plus-cookie idea from Storage Security, Encryption & Privacy.
interface AppState {
notes: { id: string; body: string }[];
syncedAt: number;
}
// Cold-start hydration: detect an empty local cache and recover from the server.
async function hydrate(loadLocal: () => Promise<AppState | null>): Promise<AppState> {
const local = await loadLocal();
if (local && local.notes.length > 0) return local;
// Local store was wiped (or first run): recover in one request.
console.info('Local cache empty — recovering state from server');
const res = await fetch('/api/state', { credentials: 'include' });
if (!res.ok) throw new Error('Recovery failed; user must re-authenticate');
const fresh = (await res.json()) as AppState;
return fresh;
}
Step 4 — Detect and log cold-start data loss
Write a small sentinel so you can distinguish a genuine first run from a post-wipe cold start, and emit a metric to confirm how often eviction bites your iOS users.
// A sentinel survives only if storage was not wiped. Its absence on a
// returning user signals eviction occurred.
const SENTINEL = 'app:install-sentinel';
function detectWipe(): 'first-run' | 'wiped' | 'intact' {
const seen = localStorage.getItem(SENTINEL);
if (seen) return 'intact';
// No sentinel: either truly first run or storage was cleared.
const hadVisit = document.cookie.includes('returning=1');
localStorage.setItem(SENTINEL, String(Date.now()));
return hadVisit ? 'wiped' : 'first-run';
}
Verification
Confirm the persistence grant and test the wipe path:
// Run after requesting persistence. On a durable origin this resolves true.
const durable = await navigator.storage.persisted();
console.assert(typeof durable === 'boolean', 'persisted() should return a boolean');
console.info('Persistent storage:', durable);
const estimate = await navigator.storage.estimate();
console.info('Usage / quota:', estimate.usage, '/', estimate.quota);
To reproduce the eviction on iOS Safari 16 or 17 without waiting a week: in Settings → Safari → Advanced → Website Data you can remove the origin’s data to simulate a wipe, then relaunch the app and confirm Step 3’s hydration recovers state in one request. For installed-PWA durability, add the app to the Home Screen, clear browser-context data, and verify the installed instance retains its IndexedDB contents.
Edge cases and a fallback
Installed context is separate. On iOS Safari 16 and 17 the installed Home Screen app and the in-browser tab can hold different storage buckets. Data written in the tab is not automatically visible to the installed app, so prompt installation early and re-hydrate from the server inside the installed context rather than expecting the tab’s data to carry over.
Private Browsing. In Safari Private Browsing, persistence is never granted and IndexedDB may be unavailable or cleared on tab close. Detect failure to open the database and fall back to in-memory state plus server recovery rather than assuming local persistence exists.
Fallback when nothing persists. If persist() is denied and the user will not install, the only robust design is to treat local storage as a disposable cache: keep the authoritative state server-side, gate it behind the longer-lived refresh cookie, and re-hydrate on every cold start where the local cache is empty. A wipe then degrades to a brief load, not a lost account.
Frequently Asked Questions
Does adding the site to the Home Screen really stop the 7-day wipe?
Yes. On iOS Safari 16 and 17, an installed PWA runs in a durable storage context exempt from the 7-day script-writable storage cap. Prompt installation early, but note the installed app and the browser tab may use separate storage buckets, so re-hydrate from the server inside the installed context.
Will navigator.storage.persist() alone prevent eviction on iOS?
Request it, but do not rely on it as the sole defense — Safari grants persistence conservatively. Pair it with server-side recovery so an evicted client re-hydrates in one request. See Debugging Storage Eviction in Progressive Web Apps.
Which storage types does the 7-day cap clear?
All script-writable storage: IndexedDB, the Cache API, localStorage, and sessionStorage, for non-installed origins after 7 days of inactivity. Cookies follow a separate, longer ITP schedule, which is why a cookie-backed server recovery path still works. The cross-browser comparison is in Browser Storage Limits Across Chrome, Firefox, and Safari.
Related
- Debugging Storage Eviction in Progressive Web Apps — confirming and diagnosing a silent wipe.
- Browser Storage Limits Across Chrome, Firefox, and Safari — how each engine’s quota and eviction differ.
- Storage Quotas & Eviction Policies — the parent guide on persistence and best-effort storage.
- Storage Security, Encryption & Privacy — the cookie-backed recovery model that survives a wipe.
- Service Worker Caching Strategies — keeping a re-hydratable cache for offline resilience.