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.

Intermediate125–150 minutesCross-platform offline semanticsFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Distinguish memory cache, persistent local cache, queued mutations and server state on Android, Apple and Web clients.

02

Predict what reads, writes, queries and listeners can do while the client network is disabled, and interpret fromCache/hasPendingWrites.

03

Configure current Web cache modes explicitly and relate them to Android/Apple defaults without assuming every platform has the same lifecycle.

04

Design offline UX that labels local optimistic state honestly and does not use Enterprise Pipeline operations as if they were offline-capable.

Execution and safety note

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.

Chapter 08 reproducibility baseline · reviewed 16 September 2026

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.

Evidence boundary

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

cache-modes.js
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 + Apple cache intent
// 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

firebase.json
{  "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json", "edition": "standard" },  "emulators": {    "firestore": { "port": 8080, "edition": "standard" },    "auth": { "port": 9099 },    "ui": { "enabled": true, "port": 4000 }  }}
firestore.rules
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; }  }}
local setup
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
offline-core.js
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.

Expected evidence, not a fabricated transcript

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

  1. Is Web persistent cache enabled automatically?
  2. What does hasPendingWrites=true mean?
  3. Can an offline query discover documents the client has never cached?
  4. Do Enterprise Pipeline operations support offline persistence?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.