Chapter 08 · Offline Persistence, Local Cache, Synchronization, Conflict Behavior, and Offline Indexes
Offline Cache Semantics for Android, Apple, and Web Clients: Reads, Writes, Listeners, and Synchronization
Understand Firestore offline cache semantics across Android, Apple and Web clients, including reads, writes, listeners, synchronization and authoritative state.
Learning outcomes
Distinguish memory cache, persistent local cache, queued mutations and server state on Android, Apple and Web clients.
Predict what reads, writes, queries and listeners can do
while the client network is disabled, and interpret
fromCache/hasPendingWrites.
Configure current Web cache modes explicitly and relate them to Android/Apple defaults without assuming every platform has the same lifecycle.
Design offline UX that labels local optimistic state honestly and does not use Enterprise Pipeline operations as if they were offline-capable.
Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.
AtlasMart continues the same environment used in Chapters
01–07: project ID demo-atlasmart-firestore,
Standard edition / Native mode /
(default) database for the mandatory lab,
Firestore emulator 127.0.0.1:8080, Authentication
emulator 127.0.0.1:9099, Emulator UI
127.0.0.1:4000, Firebase CLI
15.30.0, Firebase JavaScript SDK
12.19.0, Firebase Admin Node SDK
14.4.0 (bundling
@google-cloud/firestore 9.1.0), and Node.js 22+.
The emulator has no production location or billing contract;
any production region, pricing, SLA, IAM and managed-service
latency remain explicitly unproven by this lab.
The mandatory exercises run locally and deliberately manipulate client network state. They prove client-cache, queued-write, listener, Rules and synchronization behavior for the pinned SDK/emulator combination. They do not prove production WAN p95/p99 latency, device-OS background behavior, billing, data-recovery guarantees, real multi-region propagation, every browser storage policy, or Enterprise Pipeline behavior. Pipeline operations do not support offline persistence; Enterprise offline exercises must use Core operations.
1. AtlasMart problem: the cart must work in a subway tunnel
An AtlasMart shopper opens a product, adds it to a cart, edits a profile note, then loses connectivity. The product team wants the interface to stay responsive; security wants sensitive profile data to disappear on shared devices; operations wants to know what will actually reach the backend. Those goals are different. Firestore client SDKs maintain a local view and a mutation queue; the backend remains the authority for committed state.
Offline support is a client capability, not a second database. The cache contains documents the client has actively used. A query while offline can only evaluate against cached documents. A write can be accepted into the local mutation queue and reflected optimistically in snapshots even though the server has not yet accepted it.
| State | What AtlasMart can infer | What it cannot infer |
|---|---|---|
fromCache=true |
snapshot came from local cache | that cached data is current on the server |
hasPendingWrites=true |
snapshot includes local writes not yet acknowledged | that Rules/IAM will accept those writes |
fromCache=false, no pending writes |
listener has a server-synchronized view for that listen cycle | future writes or another device cannot change it later |
| write Promise unresolved offline | mutation is queued locally | that the mutation is committed |
2. Platform defaults are deliberately different
| Client | Default cache behavior | Restart behavior | Important boundary |
|---|---|---|---|
| Web full SDK | memory cache unless persistent cache is explicitly configured | memory cache disappears on page/process restart | persistent IndexedDB cache is supported on Chrome, Safari and Firefox; do not assume it on every browser |
| Android | persistent local cache enabled by default | cached documents/mutations survive ordinary app restart | configure memory cache explicitly when persistence is inappropriate |
| Apple | persistent local cache enabled by default | cached documents/mutations survive ordinary app restart | watchOS/App Clip availability differs; verify target support |
| Node/Admin/server | no mobile/web offline persistence contract | server process uses network/service semantics | do not copy client-cache reasoning into trusted backend code |
For Enterprise edition, the same offline client model applies only to Core operations. Pipeline operations are a different query interface and do not support offline persistence.
3. Current Web initialization: memory, persistent single-tab, persistent multi-tab
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-app.js";import { initializeFirestore, memoryLocalCache, persistentLocalCache, persistentSingleTabManager, persistentMultipleTabManager, connectFirestoreEmulator, disableNetwork, enableNetwork, doc, setDoc, getDoc, onSnapshot, serverTimestamp} from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";const app = initializeApp({projectId:"demo-atlasmart-firestore", apiKey:"demo-key", appId:"demo-app"});const mode = new URL(location.href).searchParams.get("cache") ?? "memory";const localCache = mode === "persistent-multi" ? persistentLocalCache({tabManager:persistentMultipleTabManager()}) : mode === "persistent-single" ? persistentLocalCache({tabManager:persistentSingleTabManager({})}) : memoryLocalCache();const db = initializeFirestore(app, {localCache});connectFirestoreEmulator(db, "127.0.0.1", 8080);
The modern modular API configures
FirestoreSettings.localCache during initialization.
Older enableIndexedDbPersistence() and
enableMultiTabIndexedDbPersistence() APIs are
obsolete; use the local-cache configuration above for new code.
// Android (Kotlin): persistent disk cache is the normal default.val db = Firebase.firestoreval settings = firestoreSettings { setLocalCacheSettings(persistentCacheSettings { })}db.firestoreSettings = settings// Apple (Swift): PersistentCacheSettings is the default cache type.let settings = FirestoreSettings()settings.cacheSettings = PersistentCacheSettings()let db = Firestore.firestore()db.settings = settings
4. Reproducible AtlasMart lab: observe the local/server split
{ "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json", "edition": "standard" }, "emulators": { "firestore": { "port": 8080, "edition": "standard" }, "auth": { "port": 9099 }, "ui": { "enabled": true, "port": 4000 } }}
rules_version = '2';service cloud.firestore { match /databases/{database}/documents { match /profiles/{uid} { allow read, create, update: if request.auth != null && request.auth.uid == uid; allow delete: if false; } match /carts/{uid} { allow read, create, update: if request.auth != null && request.auth.uid == uid; allow delete: if false; match /items/{productId} { allow read, create, update, delete: if request.auth != null && request.auth.uid == uid; } } match /catalogItems/{productId} { allow read: if true; allow write: if false; } match /{document=**} { allow read, write: if false; } }}
mkdir atlasmart-offline && cd atlasmart-offlinenpm init -ynpm install firebase-admin@14.4.0# Save firebase.json and firestore.rules from this lesson.printf '{"indexes":[],"fieldOverrides":[]}' > firestore.indexes.json# terminal Anpx firebase-tools@15.30.0 emulators:start --project demo-atlasmart-firestore --only firestore,auth# terminal B: serve browser filespython -m http.server 4173# then open http://127.0.0.1:4173/ in a supported browser
import { getAuth, connectAuthEmulator, signInAnonymously } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-auth.js";import { doc, setDoc, getDoc, onSnapshot, disableNetwork, enableNetwork, serverTimestamp } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";const auth=getAuth(app); connectAuthEmulator(auth,"http://127.0.0.1:9099",{disableWarnings:true});const {user}=await signInAnonymously(auth);const ref=doc(db,"profiles",user.uid);const log=(label,x)=>console.log(new Date().toISOString(),label,x);const stop=onSnapshot(ref,{includeMetadataChanges:true},snap=>log("SNAP",{ exists:snap.exists(), data:snap.data(), fromCache:snap.metadata.fromCache, hasPendingWrites:snap.metadata.hasPendingWrites}));await setDoc(ref,{displayName:"Atlas shopper",schemaVersion:1,updatedAt:serverTimestamp()},{merge:true});await disableNetwork(db);await setDoc(ref,{offlineNote:"queued locally",clientRevision:1},{merge:true});log("CACHE READ",(await getDoc(ref)).data());// Listener should expose the local mutation with hasPendingWrites=true.await enableNetwork(db);// When the server accepts the write, a later snapshot reaches hasPendingWrites=false.window.dispose=stop;
Run the same page three times with ?cache=memory,
?cache=persistent-single, and
?cache=persistent-multi. First populate the profile
while online. Then disable the SDK network, edit
offlineNote, and inspect the listener metadata. In
persistent mode, reload/restart and verify which cached state
remains. In memory mode, a fresh page instance starts without
that prior in-memory cache.
During the offline mutation, the UI can show the new value
with hasPendingWrites=true. A cache-derived
snapshot can show fromCache=true. After reconnect
and server acceptance, a later snapshot should show the
committed value without pending writes. Record your own
timestamps and sequence because callback timing is
environment-dependent.
5. Wrong approach: “offline” means “committed later no matter what”
A queued client mutation is not a durable business decision. Security Rules may change, authentication may expire, the document may be deleted, or a later conflicting write may win. Treat local UI state as pending until acknowledgement. For inventory reservations, payments, unique usernames, coupon use and other invariants, require an online trusted/transactional authority instead of promising success from the local cache.
Production judgment
Choose persistence from the product and privacy contract, not only convenience. Measure offline query latency on realistic cached data, test browser storage restrictions, define a sign-out/cache policy, instrument pending writes and reconnect errors, and decide which fields are safe to persist on a device. The next lesson shows why last-write-wins is synchronization behavior—not a substitute for transaction semantics.
Knowledge check
- Is Web persistent cache enabled automatically?
- What does
hasPendingWrites=truemean? - Can an offline query discover documents the client has never cached?
- Do Enterprise Pipeline operations support offline persistence?
- Does a successful local write UI guarantee a business invariant?
Review the answers
1. No. The Web SDK uses memory cache by default unless persistent local cache is explicitly configured.
2. The snapshot includes local writes that have not yet been acknowledged by the backend; it is not proof of server acceptance.
3. No. Offline queries evaluate the local cache, so unseen server documents are not available.
4. No. Offline persistence in Enterprise uses Core operations.
5. No. The mutation can later be rejected or lose a conflict; invariants need an authoritative online design.
Summary and next step
Firestore offline clients maintain a local synchronized view, not a second authoritative database. Lesson 2 deliberately creates conflicting offline writes and contrasts last-write-wins with transactions.
Authoritative references
- Access data offline — platform defaults, cache/query/write/listener behavior, last-write-wins and local query indexes.
- Enterprise offline access — Core-only offline boundary for Enterprise edition.
- Firebase JavaScript Firestore API — current local-cache, network and persistence APIs.
- Connect to the Firestore emulator — emulator edition configuration and cache-reset caveats.
- Firebase JavaScript SDK release notes — Web SDK 12.19.0 baseline.
- Firebase Admin Node.js release notes — Admin SDK 14.4.0 and Node.js 22+ baseline.