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.

Intermediate120–145 minutesConflict + transaction boundaryFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Explain last-write-wins as backend reconciliation of writes to the same document/field, not as semantic conflict resolution.

02

Create a deterministic two-client offline conflict by controlling reconnect order and verify the final server state.

03

Contrast queued ordinary writes/batched writes with read-dependent transactions, which fail while the client is offline.

04

Design versioning, server validation or transactional workflows for invariants that cannot tolerate blind overwrites.

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: 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 Rules addition
// 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.
two-client conflict skeleton
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.
Why this rule is intentionally unsafe

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

offline transaction must fail
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

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
  1. Temporarily add the offlineLabs/conflict-cart emulator-only rule.
  2. Open two isolated browser contexts so each owns an independent Firestore client.
  3. Disable the SDK network in both; write different quantities and record hasPendingWrites.
  4. Reconnect A and await pending writes, then reconnect B and await pending writes. Read final server state from a third trusted observer.
  5. Reset and reverse reconnect order; verify the opposite winner.
  6. Run the offline transaction example and confirm it rejects rather than calculating from stale cache.
  7. 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

  1. Does last-write-wins guarantee the user’s most important intent wins?
  2. Why control reconnect order in the lab?
  3. Can a client transaction run correctly from stale cache while offline?
  4. Does using merge:true eliminate conflicts?
  5. 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

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.