Chapter 08 · Offline Persistence, Local Cache, Synchronization, Conflict Behavior, and Offline Indexes
Test Airplane-Mode, Restart, Conflicting Writes, Reconnect, and Security-Rule Changes End to End
Test Firestore offline behavior end to end across restart, conflicts, reconnect and Security Rules changes, then define authoritative workflow states.
Learning outcomes
Execute an end-to-end offline test matrix covering airplane-mode simulation, restart, two-client conflict, reconnect and Rules regression.
Observe metadata, pending-write Promise outcomes and final server state rather than treating UI appearance as proof of success.
Keep emulator tests isolated by resetting persistent cache and distinguish emulator evidence from production browser/mobile behavior.
Produce a release checklist defining which states are authoritative, which workflows may be offline, and which require online transactional authority.
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: offline success becomes a permission failure after reconnect
The most dangerous offline bug is not “the cache is empty.” It is a screen that says “saved” while the mutation is only pending, then silently loses the change after reconnect because Rules changed or authentication no longer authorizes it. Chapter 08 ends by making that failure visible and testable.
| Scenario | Cache mode | Action | Required evidence |
|---|---|---|---|
| T1 memory offline read | memory | warm document then disable network | cached read/listener works in-process; restart loses it |
| T2 persistent restart | persistent single-tab | warm document, restart page/browser context | cached document remains available |
| T3 multi-tab queued write | persistent multi-tab | disable network in one tab, observe second tab | shared local persistence exposes coordinated pending mutation |
| T4 controlled conflict | two isolated clients | write different values offline; reconnect A then B | final server state follows controlled commit order |
| T5 Rules regression | persistent client | queue write, tighten Rules, reconnect | server rejects pending mutation; client must surface failure/rollback |
| T6 transaction offline | any client cache | disable network then run read/write transaction | transaction rejects; no false success |
| T7 clean reset | persistent | terminate and clear test persistence | next test starts without stale cached docs/pending writes |
2. Test harness: keep every step observable
{ "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 { initializeFirestore, persistentLocalCache, persistentMultipleTabManager, connectFirestoreEmulator, doc, setDoc, onSnapshot, disableNetwork, enableNetwork, waitForPendingWrites, terminate, clearIndexedDbPersistence} from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";// 1) start persistent + multi-tab; attach metadata listener// 2) go offline; set profile.offlineScenario="queued-before-rule-change"// 3) verify hasPendingWrites=true and fromCache=true/appropriate cache state// 4) while still offline, replace firestore.rules with the deny-write version// 5) let Emulator Suite reload the rules// 6) reconnect; await the write Promise / waitForPendingWrites and record rejection// 7) verify listener rolls back/re-synchronizes to accepted server state// 8) stop listeners; terminate(db); clearIndexedDbPersistence(db) before next test
Instrument every snapshot with ISO timestamp, document path,
fromCache, hasPendingWrites, visible
value and active user UID. Instrument every write Promise with a
correlation ID and final resolve/reject outcome. A UI-only
screenshot is insufficient evidence because it cannot
distinguish local pending state from committed state.
3. Failure injection: change Rules while the write is pending
Start with the normal Chapter 08 Rules, warm the profile document, disable the SDK network and queue an update. While the client remains offline, replace the rule file with a version that denies profile writes. The Emulator Suite reloads the local rules; reconnect the client and observe that the backend rejects the queued mutation.
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; } }}
rules_version = '2';service cloud.firestore { match /databases/{database}/documents { match /profiles/{uid} { allow read: if request.auth != null && request.auth.uid == uid; allow create, update: if false; allow delete: if false; } match /carts/{uid} { allow read: if request.auth != null && request.auth.uid == uid; allow create, update: if false; 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; } }}
The local view may show the queued value while offline. After reconnect, the write Promise should reject when the backend applies the new Rules, and synchronized state should return to an accepted server value. Record the actual error code/message from your run; do not hard-code a fictional transcript.
4. Restart matrix: memory versus persistent cache
Run the same warmed-document test under memory and persistent cache. A fresh memory-cache instance should not inherit prior process state. A persistent-cache instance should be able to recover cached documents across restart, subject to browser/platform storage availability. This proves persistence behavior for your test browser; it does not prove every mobile OS eviction/background policy.
5. Security Rules, Auth, App Check and IAM boundaries
The mandatory lab uses Auth emulator + Firestore Rules. App Check is intentionally not treated as offline authorization: it is an app-attestation/abuse-control signal for supported production requests, not a replacement for user authorization. Admin/server SDKs use Google credentials/IAM and bypass Firestore Security Rules, so a server-side repair script must enforce application authorization separately. The emulator exercise does not prove production App Check enforcement or IAM.
6. Release checklist: define the authoritative state
| Workflow | Offline allowed? | Authority | Reconnect action |
|---|---|---|---|
| profile preference | yes, if privacy policy permits | server-accepted profile doc | show pending; surface reject and restore server state |
| cart editing | yes as intent | server cart after sync | revalidate price/availability |
| inventory reservation | no final success offline | transaction/trusted service | attempt reservation after reconnect |
| payment | no | payment/backend state machine | fetch authoritative status |
| admin moderation | usually no client offline path | trusted backend + IAM | require online audited action |
For every offline-capable write, document: retry/idempotency semantics, how the user sees pending/rejected state, what happens on sign-out, which cached fields are sensitive, and how support staff can diagnose an offline conflict without collecting private document contents in logs.
7. Cleanup and repeatability
- Restore the normal
firestore.rules. - Wait for pending writes to resolve/reject; capture only synthetic test values.
- Detach all listeners.
- Terminate the Firestore instance and clear IndexedDB persistence for this test origin.
- Stop/restart Emulator Suite if you want an empty backend; remember that emulator shutdown alone does not clear client cache.
- Rerun T1–T7. The outcome class should match even if callback timing differs.
Production judgment
Offline readiness is a cross-layer property: cache policy, local UX, Rules/Auth state, conflict model, transaction boundaries, device privacy, telemetry, and incident recovery all matter. Test p95/p99 only on defined device/network/cache scenarios; never extrapolate from localhost. The next chapter moves to transactions, batched writes, atomic transforms, retry semantics and contention—the online mechanisms needed when last-write-wins is not enough.
Knowledge check
- What proves an offline edit was committed?
- Why change Rules while a write is pending?
- What must be cleared between persistent-cache emulator tests?
- Can App Check replace Security Rules authorization?
- Which next mechanism handles read-dependent invariants?
Review the answers
1. A local snapshot does not. You need successful backend acknowledgement/synchronization and no pending writes, plus application-specific validation where required.
2. It proves the app handles a realistic reconnect-time authorization failure instead of assuming queued writes always succeed.
3. Client persistent cache/pending state as well as emulator backend state, depending on the scenario.
4. No. App Check is not user authorization; Rules/IAM and application authorization remain necessary.
5. Transactions/trusted transactional workflows, covered in Chapter 09.
Summary and next step
You now have a deterministic offline test matrix and an explicit authority model. Chapter 09 builds the transaction/batch/atomic-transform tools used when offline last-write-wins cannot satisfy correctness.
Authoritative references
- Access data offline — cache, synchronization, last-write-wins, local indexes and sensitive-data guidance.
- Transactions and batched writes — offline transaction boundary and retry semantics.
- Get started with Firestore Security Rules — client authorization model used in the regression test.
- Connect to Firestore Emulator — local Rules/emulator testing and cache caveats.
- JavaScript Firestore API — network, pending-write, terminate and clear-persistence APIs.
- Enterprise offline access — Core-only offline support boundary.