Chapter 07 · Realtime Listeners: Snapshots, Change Events, Metadata, Latency, and Listener Lifecycle

Design a Realtime Screen with Correct Offline / Online State, Security, Pagination, and Cost Controls

Design a production-oriented Firestore realtime screen with bounded listeners, offline/online labels, Rules-compatible queries, cursor pagination, lifecycle cleanup, and cost controls.

Intermediate130–155 minutesRealtime screen capstoneFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Design a realtime AtlasMart screen whose live region is bounded and whose historical region uses cursor pagination instead of an unbounded listener.

02

Expose cached/pending/server-current state honestly and stop listeners on route/user lifecycle changes.

03

Keep Security Rules/query constraints compatible and separate client Rules from trusted Admin/IAM execution.

04

Create a listener runbook with cost controls, failure injection, observability, and an explicit polling/event-infrastructure escape hatch.

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 07 reproducibility baseline · reviewed 15 September 2026

AtlasMart continues the same environment used in Chapters 01–06: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database for the main 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+. Browser examples import the pinned Web SDK from Google-hosted modules and connect only to local emulators.

Evidence boundary

The authoring environment did not execute a billed Firestore database or collect production listener telemetry. Emulator steps below are deterministic correctness exercises; shown event sequences are expected invariants, not captured benchmark evidence. The emulator cannot prove production billing, WAN reconnect behavior, managed index enforcement, production latency percentiles, or all mobile background/OS lifecycle behavior.

1. Capstone problem: live current order, paged history, selective inventory

AtlasMart's order center has three needs with different freshness targets: the current order status should update quickly; order history can be paged; seller inventory only needs the lowest-stock items for the logged-in seller. The correct design is not “listen to all orders and all products forever.” It assigns a separate read contract to each region.

Screen region Read contract Lifecycle Why
Current order card One document listener Attach while card visible/current order exists Small live surface; domain status changes matter
Seller low-stock panel Bounded selective query listener Attach on seller dashboard; detach on exit Fresh top-N operational view
Order history One-shot cursor pages Fetch on initial load/scroll Past orders do not need constant live churn
Analytics totals Aggregation/analytics path Scheduled/on demand Do not maintain huge client listener for analytics

2. Security/query contract first

Mobile/Web listeners are evaluated through Firestore Security Rules. Rules are not post-filters: the query must be provably compatible with allowed access. A broad collection listener that would include other users' orders cannot be made safe by filtering unauthorized rows in JavaScript. Model the path/query so the backend can authorize the entire requested result.

Rules excerpt · own order subtree, public catalog reads
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /catalogItems/{productId} {      allow read: if true;      allow write: if false;    }    match /profiles/{uid} {      allow read, create, update: if request.auth != null && request.auth.uid == uid;      allow delete: if false;      match /orders/{orderId} {        allow read, create: if request.auth != null && request.auth.uid == uid;        allow update: if request.auth != null          && request.auth.uid == uid          && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['customerNote', 'clientTouchedAt']);        allow delete: if false;      }    }    match /{document=**} { allow read, write: if false; }  }}

Trusted Admin/server code is different: server libraries authenticate with IAM/credentials and bypass mobile/Web Security Rules. Never move the same client rule assumptions into an Admin service. The emulator Admin pulse is trusted intentionally so it can simulate fulfillment/inventory changes.

3. Build the current-order + inventory live regions

order-center.js · browser module core
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-app.js";import { getAuth, connectAuthEmulator, signInAnonymously } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-auth.js";import {  getFirestore, connectFirestoreEmulator, collection, doc, query, where, orderBy, limit,  onSnapshot, setDoc, updateDoc, serverTimestamp, disableNetwork, enableNetwork} 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 db = getFirestore(app); connectFirestoreEmulator(db, "127.0.0.1", 8080);const auth = getAuth(app); connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings:true });const credential = await signInAnonymously(auth);const uid = credential.user.uid;console.log("uid", uid);const state={stops:new Map(),live:{},history:[]};function own(name, attach) { state.stops.get(name)?.(); const stop=attach(); state.stops.set(name,stop); }function stopAll() { for(const stop of state.stops.values()) stop(); state.stops.clear(); }function label(meta) { return meta.hasPendingWrites ? "saving" : (meta.fromCache ? "cached" : "synced"); }const orderRef=doc(db,`profiles/${uid}/orders/o-live-1`);await setDoc(orderRef,{status:"created",customerNote:"",clientTouchedAt:serverTimestamp()},{merge:true});own("current-order",()=>onSnapshot(orderRef,{includeMetadataChanges:true},snap=>{  state.live.order={data:snap.data(),sync:label(snap.metadata)}; render(state);},showError));const lowStock=query(collection(db,"catalogItems"),where("sellerId","==","seller-a"),orderBy("stock","asc"),limit(5));own("low-stock",()=>onSnapshot(lowStock,{includeMetadataChanges:true},snap=>{  state.live.stock={rows:snap.docs.map(d=>({id:d.id,...d.data()})),sync:label(snap.metadata)}; render(state);},showError));window.atlasmartDispose=stopAll;

4. History is pagination, not an unbounded live listener

Chapter 05 established stable cursor pagination. Keep that contract. Load a page of historical orders with a deterministic order and cursor, then optionally refresh on user action. If one historical order becomes “current,” attach a document listener to that order rather than turning the entire history into a listener.

history pattern · conceptual Web code
const pageQ = query(  collection(db, `profiles/${uid}/orders`),  orderBy("createdAt","desc"),  orderBy(documentId()),  limit(20));const page = await getDocs(pageQ);// Store the final DocumentSnapshot as the next cursor.// Next page: startAfter(page.docs.at(-1)) with the exact same ordering.

5. Offline/online truth table

Observed state Current-order UX Allow irreversible action?
pending=true Show optimistic value + “Saving…” Usually no if action requires confirmed inventory/payment
cache=true, pending=false Show cached value + stale/offline indicator Only if product contract tolerates stale data
cache=false, pending=false Show synced value Still validate domain invariant server-side
listener error Stop “live” indicator; show recoverable/error state No; diagnose auth/rules/index/network cause

Chapter 08 will go deeper into persistence and conflict behavior. Here the rule is simpler: live UI must tell the truth about whether state is local/cached/server-current.

6. Cost/control budget before deployment

listener-budget.yaml · example operational contract
screen: order-centeredition: standardmode: native-corelive_regions:  - name: current-order    shape: document    max_per_screen: 1  - name: low-stock    shape: query    filters: sellerId == currentSeller    order: stock asc    limit: 5non_live_regions:  - name: order-history    shape: cursor-pages    page_size: 20controls:  detach_on_route_exit: true  detach_on_sign_out: true  metadata_changes: true  active_listener_metric: true  reconnect_metric: true  production_budget_alerts: truenotes:  - Standard listener pricing and Rules-dependent reads must be rechecked for deployment region.  - Enterprise uses read units + realtime-update units; do not reuse Standard estimates.

7. Failure-injection matrix

Injection Expected evidence Cleanup
Disable SDK network, edit note pending metadata; cached/local UI; no false “server saved” Re-enable; await acknowledgement/rejection
Admin changes seller-b item seller-a scoped listener does not churn unless result contract affected Reset fixture
Admin changes seller-a low-stock item ordered query snapshot updates coherently Reset fixture
Navigate away/back repeatedly active-listener count returns to baseline each exit Call dispose; assert zero
Temporary deny Rules for order update client write/listener error path is visible; server Admin unaffected by client Rules Restore Rules; restart clean emulator

8. When not to use a realtime listener

  • Use one-shot reads/pagination when periodic freshness is acceptable.
  • Use server-side scheduled materialization/aggregation when the client would otherwise watch huge collections for totals.
  • Use durable event/messaging systems for exactly-once-like processing requirements, retention, replay, consumer groups, or irreversible side effects; the Firestore listener is synchronized state, not a durable event queue.
  • In Enterprise Native, use Core operations if realtime/offline is needed; Pipeline queries themselves do not support realtime listeners.
  • For MongoDB compatibility, evaluate its Preview change-stream feature separately; do not port Web SDK metadata assumptions.

9. Acceptance checklist

  1. Start Firestore/Auth emulators with the pinned Standard config and seed the catalog.
  2. Open the order-center app; confirm exactly two live owners: current order and seller-a low stock.
  3. Run Admin pulses and verify only query-relevant changes affect the seller panel.
  4. Disable network, write a customer note, and verify pending/cached labels; re-enable and observe resolution.
  5. Navigate/dispose and assert zero owned listeners.
  6. Fetch history by cursor pages, not a history-wide listener.
  7. Record what the emulator did not prove: production index enforcement, billing, WAN reconnect behavior, p95/p99 latency, background mobile lifecycle.
  8. For a managed disposable project only, capture actual usage/billing telemetry and budget alerts before extrapolating cost.

Production judgment and runbook

Ship only after you can answer: Which regions are live? Who owns each listener? What is the maximum result size? What does cached/pending mean in the UI? Which Rules authorize the query? What happens after sign-out/route exit? What reconnect rate do you observe? What is the Standard or Enterprise cost model? What metric tells you freshness is degrading? What is the fallback if realtime fan-out becomes uneconomic?

Backup/recovery does not preserve a user's active listener session; after recovery/restart, clients establish new read state. Therefore recovery testing must validate that Rules, indexes, data, and app versions still permit the expected query/listener contracts.

Knowledge check

  1. Why is the order-history region paged instead of live?
  2. Why can’t client-side filtering make a broad orders listener secure?
  3. What two listener ownership events are mandatory in this capstone?
  4. What should a screen do when a listener returns a terminal error?
  5. What is the Chapter 08 bridge?
Review the answers

1. Its freshness requirement is weaker; cursor pages bound reads and lifecycle while the current order gets the focused realtime listener.

2. Security Rules are not post-filters; the backend must be able to prove the whole query is authorized.

3. Route/view disposal and authentication/session changes such as sign-out.

4. Stop claiming the region is live, surface/recover appropriately, record the error, and fix auth/rules/index/query cause before reattaching.

5. Offline persistence, local-cache durability, conflict behavior, multi-tab/process semantics, and the distinction between local writes and server transactions.

Summary and next step

Chapter 07 ends with a bounded, metadata-aware, lifecycle-owned realtime design. Chapter 08 keeps the same listener observations but removes the assumption of continuous connectivity and studies cache persistence, queued writes, conflicts, and offline indexes explicitly.

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.