Chapter 27 · Production Capstone: Design, Secure, Scale, Search, Recover, and Operate a Firestore Application

Implement Documents / Indexes / Queries / Listeners / Transactions, Typed SDK Access, Security Rules, and Trusted Backend Paths

Implement AtlasMart Core documents, indexes, typed access, Rules, listeners and an idempotent trusted checkout path with separate client/server authorization.

Advanced · 180–240 minutesdocuments · indexes · queries · listeners · transactions · Rules · IAMNode 22+ · Firebase CLI course baseline 15.30.0 · JS SDK 12.19.0 · Admin SDK 14.4.0 · rules-unit-testing 5.0.2Mandatory capstone demo project + Emulator Suite/no-cost · managed production verification explicitly separatedLast reviewed: 17 September 2026

1. AtlasMart implementation: one request, two trust paths

The capstone becomes real when the same application clearly separates what an untrusted browser/mobile client may do from what only a trusted backend may do. A customer can browse products, edit their own cart and listen to their own order. The customer cannot decrement inventory, set an order to paid, rewrite prices, or use an Admin SDK credential. Checkout is a backend business operation authenticated with the user identity, authorized by application logic, and executed with server IAM.

Learning outcomes
  • Implement stable document contracts and indexes for AtlasMart access patterns.
  • Use typed converters/validators so schemaVersion and money/identity fields cross SDK boundaries predictably.
  • Make Rules queries provably compatible rather than filtering unauthorized results client-side.
  • Implement checkout as an idempotent trusted transaction with no irreversible side effects inside the retryable callback.
  • Test listeners, transaction conflicts, Rules denials and server-path authorization separately.
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 27 reproducibility baseline · reviewed 17 September 2026

AtlasMart keeps project ID demo-atlasmart-firestore, Standard edition / Native mode / Core operations, database (default), Node.js 22+, Firebase CLI course baseline 15.30.0, Firebase JavaScript SDK 12.19.0, Firebase Admin Node SDK 14.4.0, @firebase/rules-unit-testing 5.0.2, Firestore emulator 127.0.0.1:8080, Auth emulator 127.0.0.1:9099, and Emulator UI 127.0.0.1:4000. The mandatory capstone uses only a demo- project and local tooling. Managed location, production IAM/App Check, real composite/vector indexes, Query Explain/Insights, billing, quotas, Key Visualizer, scheduled backups/PITR, CMEK/network controls, Enterprise/Pipeline, and MongoDB-compatibility validation are optional managed evidence and are never inferred from emulator success. Where a managed Standard example needs a concrete location, this chapter uses us-central1 only as an explicit example—not a universal recommendation.

2. Source-controlled runtime configuration

firebase.json
{  "firestore": {    "rules": "firestore.rules",    "indexes": "firestore.indexes.json",    "edition": "standard"  },  "emulators": {    "auth": { "host": "127.0.0.1", "port": 9099 },    "firestore": { "host": "127.0.0.1", "port": 8080 },    "ui": { "host": "127.0.0.1", "port": 4000 }  }}
firestore.indexes.json
{  "indexes": [    {      "collectionGroup": "products",      "queryScope": "COLLECTION",      "fields": [        { "fieldPath": "active", "order": "ASCENDING" },        { "fieldPath": "categoryId", "order": "ASCENDING" },        { "fieldPath": "updatedAt", "order": "DESCENDING" }      ]    },    {      "collectionGroup": "orders",      "queryScope": "COLLECTION",      "fields": [        { "fieldPath": "ownerUid", "order": "ASCENDING" },        { "fieldPath": "createdAt", "order": "DESCENDING" }      ]    }  ],  "fieldOverrides": [    {      "collectionGroup": "events",      "fieldPath": "expireAt",      "indexes": []    }  ]}
firestore.rules
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    function signedIn() { return request.auth != null; }    function tenant() { return request.auth.token.tenantId; }    function sameTenant(t) { return signedIn() && tenant() == t; }    function owns(uid) { return signedIn() && request.auth.uid == uid; }    match /products/{productId} {      allow read: if resource.data.active == true                  && resource.data.visibility == "public";      allow write: if false; // trusted backend only    }    match /carts/{uid} {      allow read, create, update: if owns(uid)        && request.resource.data.ownerUid == uid        && request.resource.data.schemaVersion in [1, 2];      allow delete: if owns(uid);      match /items/{itemId} {        allow read, write: if owns(uid)          && request.resource.data.ownerUid == uid;      }    }    match /orders/{orderId} {      allow read: if signedIn() && resource.data.ownerUid == request.auth.uid;      allow write: if false; // checkout/status writes use trusted backend + IAM    }  }}

The index file is a Standard Native contract. The emulator does not track compound indexes, so local query success is not proof that the composite index exists in production. The Rules file protects mobile/web client requests. The trusted backend uses the Admin/server library and therefore requires IAM plus application-layer authorization; it does not become subject to these Rules merely because it is acting for a user.

3. Typed SDK access: validate at the boundary

src/product-model.mjs
export function decodeProduct(id, data) {  if (!data || data.schemaVersion !== 2) throw new Error(`unsupported product ${id}`);  if (!Number.isInteger(data.priceCents) || data.priceCents < 0) throw new Error("bad price");  if (typeof data.active !== "boolean") throw new Error("bad active flag");  return Object.freeze({    id,    categoryId: String(data.categoryId),    name: String(data.name),    priceCents: data.priceCents,    active: data.active,    updatedAt: data.updatedAt  });}export function encodeCartItem(item) {  if (!Number.isInteger(item.quantity) || item.quantity < 1 || item.quantity > 20) {    throw new Error("quantity out of range");  }  return {    ownerUid: item.ownerUid,    productId: item.productId,    quantity: item.quantity,    observedPriceCents: item.observedPriceCents,    updatedAt: item.updatedAt,    schemaVersion: 2  };}

A converter/validator is not a substitute for Rules or server validation. It gives the application a typed failure boundary and prevents silent schema drift. An attacker can bypass client code, so authorization and protected-field constraints remain on the trusted boundary.

4. Query contract: Rules are not filters

Client browse query
import { collection, query, where, orderBy, limit, getDocs } from "firebase/firestore";const q = query(  collection(db, "products"),  where("active", "==", true),  where("visibility", "==", "public"),  where("categoryId", "==", "drinkware"),  orderBy("updatedAt", "desc"),  limit(20));const snap = await getDocs(q);

The query must be compatible with both Rules and indexes. A client query that fetches all products and then filters hidden rows in JavaScript is not merely inefficient; it is denied when the possible result set contains documents the Rules do not authorize.

Production-only index proof

The emulator executes valid queries without tracking compound indexes. A managed staging database must prove the required composite index is deployed and ready before enabling this query in production.

5. Realtime order status: cache metadata is product state

Owner order listener
import { doc, onSnapshot } from "firebase/firestore";const stop = onSnapshot(  doc(db, "orders", orderId),  { includeMetadataChanges: true },  snap => {    if (!snap.exists()) return renderMissing();    const m = snap.metadata;    renderOrder({      ...snap.data(),      source: m.fromCache ? "cache" : "server",      pendingLocalWrites: m.hasPendingWrites    });  });// call stop() when the screen unmounts

The listener is not a free push channel. Its initial result and later changes follow current Firestore billing semantics. The UI must also distinguish cache/server and pending-write state instead of presenting cached data as fresh server confirmation.

6. Trusted checkout: user identity enters; privileged state change stays on server

Server checkout skeleton
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";process.env.GCLOUD_PROJECT = "demo-atlasmart-firestore";import { initializeApp } from "firebase-admin/app";import { getAuth } from "firebase-admin/auth";import { getFirestore, FieldValue } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();export async function checkout(idToken, key, skuId, qty) {  const decoded = await getAuth().verifyIdToken(idToken);  const uid = decoded.uid;  const orderRef = db.collection("orders").doc(`${uid}_${key}`);  const inventoryRef = db.collection("inventory").doc(skuId);  return db.runTransaction(async tx => {    const [existing, inventory] = await Promise.all([      tx.get(orderRef), tx.get(inventoryRef)    ]);    if (existing.exists) return { orderId: orderRef.id, duplicate: true };    if (!inventory.exists || inventory.get("available") < qty) {      throw new Error("OUT_OF_STOCK");    }    tx.update(inventoryRef, {      available: FieldValue.increment(-qty),      reserved: FieldValue.increment(qty),      updatedAt: FieldValue.serverTimestamp()    });    tx.create(orderRef, {      ownerUid: uid,      idempotencyKey: key,      status: "reserved",      skuId, quantity: qty,      createdAt: FieldValue.serverTimestamp(),      schemaVersion: 2    });    return { orderId: orderRef.id, duplicate: false };  });}

A transaction callback can execute more than once. Therefore sending email, charging a payment provider or publishing an irreversible external event inside the callback is unsafe. Persist the durable order/outbox state atomically; process external side effects idempotently after commit.

7. Failure injection: prove the trust boundary

Injected mistake Expected signal Repair
Client writes orders/o1.status="paid". Rules test denies write. Status transitions remain backend-only.
Client broad-queries products then filters private items locally. Rules deny query. Query includes provably safe predicates.
Two checkouts race for one remaining SKU. One transaction succeeds; competing path retries/fails invariant. Keep inventory/order invariant in transaction and surface retry-safe error.
Same idempotency key submitted twice. Second call returns existing order, no second decrement. Deterministic order/idempotency record.
Transaction callback sends email before commit. Failure injection demonstrates duplicate email on retry. Move side effect to durable idempotent post-commit worker.

8. Rules and server authorization are separate tests

Rules test intent
const alice = testEnv.authenticatedContext("alice", { tenantId: "t-red" });const mallory = testEnv.authenticatedContext("mallory", { tenantId: "t-blue" });await assertSucceeds(alice.firestore().doc("carts/alice").get());await assertFails(mallory.firestore().doc("carts/alice").get());await assertFails(alice.firestore().doc("orders/o-1").set({ status: "paid" }));

Separately test the backend authorization function that maps a verified user token to allowed checkout resources. Admin SDK success proves only that the backend identity can reach Firestore; it does not prove the user should be allowed to invoke that business operation.

9. Mandatory lab and verification checklist

  1. Start Auth + Firestore emulators with demo-atlasmart-firestore.
  2. Deploy/load the local Rules and seed product/cart/inventory fixtures.
  3. Assert public product browse succeeds and broad unsafe query fails Rules.
  4. Assert Alice can edit her cart and Mallory cannot read it.
  5. Run two concurrent checkout attempts for one remaining SKU and assert no oversell.
  6. Replay the winning checkout with the same idempotency key and assert inventory is not decremented twice.
  7. Attach an order listener, record cache/server metadata and call unsubscribe.
  8. Record VERIFY_MANAGED for composite-index readiness, IAM/App Check, production transaction contention and billing evidence.
Expected state

Client capabilities are bounded by Rules; backend capabilities are bounded by verified user/business authorization plus IAM; the inventory invariant survives contention and replay; no emulator result is mislabeled as production capacity/index evidence.

Production judgment and bridge

The core application is now intentionally boring: documents, query contracts, a small number of indexes, a clear listener, and one authoritative transaction boundary. Lesson 3 asks whether aggregation, semantic vectors, Enterprise Pipeline or MongoDB compatibility add enough value to justify new operational surfaces.

Knowledge check

  1. Why must the product query repeat security-relevant constraints?
  2. Why does Admin SDK access not prove end-user authorization?
  3. Why is email unsafe inside a transaction callback?
  4. What does fromCache tell the UI?
  5. Why must composite-index readiness be checked outside the emulator?
Review the answers

1. Rules evaluate a query against its possible result set; they are not post-query filters.

2. Server libraries bypass Firestore Rules and use IAM, so the application must authorize the verified user action itself.

3. The transaction can retry, so the callback can execute more than once.

4. The snapshot was served from local cache rather than confirmed current server state.

5. The emulator does not track compound indexes and can run a query that production would reject until the index exists.

Summary and next step

This lesson established the working contract for Implement Documents/Indexes/Queries/Listeners/Transactions, Typed SDK Access, Security Rules, and Trusted Backend Paths. Keep its edition/mode assumptions, trust boundary, verification evidence, and operational constraints explicit when reusing the pattern.

Next, continue to Add Aggregation, Vector Retrieval, Enterprise Pipeline or MongoDB Compatibility Only Where Requirements Justify Them.

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.