Chapter 08 · Offline Persistence, Local Cache, Synchronization, Conflict Behavior, and Offline Indexes
Last-Write-Wins Behavior for Multiple Offline Changes and Where Transactions Differ
Understand Firestore last-write-wins synchronization for conflicting offline changes and why read-dependent transactions fail offline.
Learning outcomes
Explain last-write-wins as backend reconciliation of writes to the same document/field, not as semantic conflict resolution.
Create a deterministic two-client offline conflict by controlling reconnect order and verify the final server state.
Contrast queued ordinary writes/batched writes with read-dependent transactions, which fail while the client is offline.
Design versioning, server validation or transactional workflows for invariants that cannot tolerate blind overwrites.
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: two devices edit one cart
A shopper changes the same cart quantity on a phone and laptop while both are offline. Each device can display its own optimistic value. When they reconnect, Firestore synchronizes both mutation streams. For multiple changes to the same document, the documented conflict behavior is last write wins. That phrase describes the final committed state after synchronization; it does not mean Firestore understands which quantity is “correct” for the business.
For a simple preference such as theme or display label, last-write-wins may be acceptable. For stock reservations, account balances, redemption limits or state-machine transitions, it is usually not.
| Pattern | Offline behavior | Conflict meaning |
|---|---|---|
ordinary setDoc/updateDoc |
queued locally; UI may update optimistically | later accepted write can replace an earlier value |
| atomic field transform | queued as a transform where supported by the client SDK | reduces some read-modify-write races but still requires domain reasoning |
| batched write without reads | client SDK can queue atomic writes for later commit | atomicity is across the batch when committed; no read-time invariant check |
| transaction | requires reads and retry against current server data | fails while client is offline |
2. Make the winner deterministic by controlling reconnect order
Do not use wall-clock timestamps to “prove” a race. In the emulator, make two browser contexts write offline, then reconnect them in a chosen order and await pending-write completion. The later committed write in that controlled sequence becomes the final value.
// TEMPORARY emulator-only rule for the two-window conflict experiment.match /offlineLabs/{docId} { allow read, write: if docId == "conflict-cart";}// Remove this block immediately after the lab. Never deploy it to production.
import { doc, setDoc, getDoc, disableNetwork, enableNetwork, waitForPendingWrites } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";const shared=doc(db,"offlineLabs","conflict-cart"); // emulator-only fixture// Window Aawait disableNetwork(db);await setDoc(shared,{quantity:2,writer:"A"},{merge:true});// Window B (separate browser context)await disableNetwork(db);await setDoc(shared,{quantity:7,writer:"B"},{merge:true});// Reconnect A first and wait for acknowledgement.await enableNetwork(db); await waitForPendingWrites(db);// Then reconnect B and wait for acknowledgement.await enableNetwork(db); await waitForPendingWrites(db);// Trusted observer should now see B's final write in this controlled order.
The open lab path exists only inside project
demo-atlasmart-firestore on the emulator. It is a
controlled failure-injection surface. Remove the rule when the
experiment ends. Production client-writable shared documents
require authentication, authorization, validation, abuse
controls and usually a more explicit conflict protocol.
3. What “last” does and does not buy you
Last-write-wins gives convergence for conflicting writes, not intent preservation. A full-document overwrite can erase fields changed by another client; a field-level merge narrows the collision surface but does not solve semantic conflicts on the same field. A client timestamp is not a trustworthy conflict oracle because clocks drift and malicious clients can forge values.
If AtlasMart needs merge semantics, encode them: immutable event documents, server-side version numbers, per-field ownership, transactions, or a trusted command service. If it needs human resolution, store both versions and expose the conflict instead of silently discarding one.
4. Transactions are different because they need authoritative reads
import { runTransaction, doc } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";await disableNetwork(db);try { await runTransaction(db, async tx => { const ref=doc(db,"inventory","p-1001"); const snap=await tx.get(ref); tx.update(ref,{stock:snap.data().stock-1}); });} catch (e) { console.log("expected offline transaction failure", e.code, e.message);}
A Firestore transaction reads one or more documents, computes a change, and may be retried when concurrent edits invalidate the read set. That algorithm cannot promise correctness from stale local cache; therefore client transactions fail offline. This is a useful design signal. If checkout correctness depends on the latest inventory state, offline “success” should not be shown.
5. Repair patterns for AtlasMart
| Requirement | Safer pattern | Offline UX |
|---|---|---|
| profile nickname | field merge + last-write-wins may be acceptable | show pending badge; allow later correction |
| cart quantity | store item quantity per product; server revalidates at checkout | allow local edits but label cart as not reserved |
| inventory reservation | online transaction/trusted service with idempotency key | collect intent offline; reserve only after reconnect |
| payment transition | trusted backend state machine | never claim paid from an offline client mutation |
| collaborative document | explicit version/conflict model or CRDT-like application layer | surface merge/conflict state |
6. Reproducible lab sequence
{ "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
-
Temporarily add the
offlineLabs/conflict-cartemulator-only rule. - Open two isolated browser contexts so each owns an independent Firestore client.
-
Disable the SDK network in both; write different quantities
and record
hasPendingWrites. - Reconnect A and await pending writes, then reconnect B and await pending writes. Read final server state from a third trusted observer.
- Reset and reverse reconnect order; verify the opposite winner.
- Run the offline transaction example and confirm it rejects rather than calculating from stale cache.
- Remove the temporary rule and restart/reset the emulator.
Production judgment
Write a conflict policy per field or workflow. Record whether the system tolerates last-write-wins, requires merge semantics, or needs serializable business logic. Track rejected pending writes and conflict-repair rates as product signals. The next lesson moves from conflict semantics to the privacy/lifecycle consequences of choosing memory versus persistent cache.
Knowledge check
- Does last-write-wins guarantee the user’s most important intent wins?
- Why control reconnect order in the lab?
- Can a client transaction run correctly from stale cache while offline?
- Does using
merge:trueeliminate conflicts? - What should AtlasMart do for inventory reservations?
Review the answers
1. No. It only describes convergence of multiple writes; the database does not know business priority.
2. It removes ambiguous timing and makes the final committed order observable and repeatable.
3. No. Firestore client transactions fail offline because they need authoritative reads/retry semantics.
4. No. It reduces overwrite scope but two writers can still conflict on the same field.
5. Treat offline edits as intent and perform authoritative validation/reservation online, typically through a transaction or trusted service.
Summary and next step
Last-write-wins is a synchronization rule, not a business invariant engine. Lesson 3 chooses cache modes deliberately and treats sign-out/privacy as data-lifecycle requirements.
Authoritative references
- Access data offline — queued changes and documented last-write-wins behavior.
- Transactions and batched writes — transaction retry semantics and the explicit offline failure boundary.
- Firebase JavaScript Firestore API — network controls and pending-write APIs used in the lab.
- Firestore Emulator Suite — safe local failure-injection environment.