Chapter 02 · Documents, Collections, Fields, References, Maps, Arrays, Timestamps, GeoPoints, and Limits

Inspect and Validate a Document Schema Across Web / Mobile / Server SDK Serialization Boundaries

Build a cross-SDK schema contract that survives Web, mobile, and trusted server serialization differences, preserves Firestore-native types, and exposes missing/null, number, timestamp, reference, bytes, map, and security-boundary mistakes.

Beginner → Advanced90–120 minutesAtlasMart emulator-first labFirebase CLI 15.30.0 · Web SDK 12.19.0 · Admin Node 14.4.0 · Node.js 22+Firestore Standard Native semantics unless explicitly labeled EnterpriseLast reviewed: September 2026

Learning outcomes

Chapter 02 ends by turning the document model into executable evidence. AtlasMart cannot call a schema portable merely because TypeScript compiles or one Admin script succeeds. Browser/mobile clients, server SDKs, security rules, and exports all expose the same stored document through different host-language types and trust boundaries. The schema contract must compare semantics, not object-class names.

01

Define a canonical AtlasMart schema contract for paths, types, optional/null policy, indexing, and writer trust level.

02

Compare Web/mobile/server representations of timestamps, references, GeoPoints, bytes, numbers, arrays, and maps.

03

Build round-trip assertions that canonicalize SDK wrapper types before comparison.

04

Detect dangerous serialization drift such as int64 precision loss, Date/string coercion, undefined fields, and server-only fields entering client writes.

05

Produce a Chapter 02 acceptance checklist that bridges cleanly into access-pattern-driven modeling in Chapter 03.

Chapter 02 baseline reviewed 14 September 2026

The lab continues Chapter 01 exactly: project demo-atlasmart-firestore; Firestore emulator 127.0.0.1:8080; Auth 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.js SDK 14.4.0; Node.js 22 or newer. Standard Native semantics are the default. Enterprise-only differences are labeled explicitly.

Execution and evidence note

These lessons are authored against current official documentation and the deterministic emulator design from Chapter 01. This generation environment does not run the Firebase Emulator Suite or install the pinned npm dependencies, so command output is described by invariant and expected shape rather than presented as captured execution. The emulator is evidence for local application behavior, not proof of production quotas, regional latency, backend index topology, or billing.

1. The schema contract must describe semantics and trust

Firestore does not require every document in a collection to have the same fields or types, so AtlasMart must own that consistency. For each field, record the Firestore semantic type, whether missing and null are allowed, which actors may write it, whether clients can read it, whether it is indexed, and what migration preserves backward compatibility.

Field Semantic contract Writer Index intent
name Required string; ≤ 200 UTF-8 bytes by AtlasMart policy Trusted server/admin catalog workflow Indexed
priceMinor Required safe-range integer in minor currency units Trusted server Indexed if range/sort needed
updatedAt Required Firestore Timestamp Trusted server transform Indexed
categoryRef Optional DocumentReference Trusted server Only if query contract requires
warehouse Optional GeoPoint Trusted server Query design dependent
description Required string, long text Trusted server Exempt
attributes Bounded map with validated keys Trusted server/seller service Exempt until stable query subfields exist
internalRiskScore Server-only number; never accepted from client Trusted server only Usually exempt unless server query needs it

2. SDKs preserve Firestore meaning through different host-language wrappers

A timestamp is conceptually the same database value whether a browser exposes a Web Timestamp, an Android/Apple SDK exposes its platform timestamp type, or the server library exposes a Google Cloud Firestore timestamp object. The same applies to DocumentReference, GeoPoint, and bytes. Tests should compare path/seconds/coordinates/byte content—not require identical class names across languages.

Firestore value Web client Mobile client concept Node/Admin server Portable assertion
Timestamp Firestore Timestamp Platform Firestore timestamp/date bridge Firestore Timestamp Compare seconds + subsecond value after documented precision.
Reference DocumentReference Platform document-reference wrapper DocumentReference Compare database/path intent, not object identity.
GeoPoint GeoPoint Platform GeoPoint wrapper GeoPoint Compare latitude/longitude.
Bytes Web Bytes Platform blob/byte wrapper Typically Buffer/byte representation Compare raw bytes/base64.
Integer JavaScript Number representation Language-dependent integer type JavaScript Number representation Assert domain range; do not rely on impossible JS int64 precision.
Map/array JS object/array with wrappers inside Platform map/list/custom model JS object/array Canonicalize recursively, preserving nested types.

3. Build a canonical encoder before comparing SDK output

Raw JSON serialization is a poor conformance test because SDK wrapper objects can serialize differently or lose type information. AtlasMart defines a canonical semantic view. Each SDK-specific harness converts native values into the same neutral representation, then compares that output to a checked-in fixture.

canonicalize-web.mjs · explicit type tags
import { Timestamp, GeoPoint, Bytes, DocumentReference } from "firebase/firestore";export function canonicalWeb(value) {  if (value instanceof Timestamp) return { $timestamp: [value.seconds, value.nanoseconds] };  if (value instanceof GeoPoint) return { $geo: [value.latitude, value.longitude] };  if (value instanceof Bytes) return { $bytes: value.toBase64() };  if (value instanceof DocumentReference) return { $ref: value.path };  if (Array.isArray(value)) return value.map(canonicalWeb);  if (value && typeof value === "object") {    return Object.fromEntries(Object.keys(value).sort().map(k => [k, canonicalWeb(value[k])]));  }  return value;}
canonicalize-admin.mjs · trusted server counterpart
import { Timestamp, GeoPoint, DocumentReference } from "firebase-admin/firestore";export function canonicalAdmin(value) {  if (value instanceof Timestamp) return { $timestamp: [value.seconds, value.nanoseconds] };  if (value instanceof GeoPoint) return { $geo: [value.latitude, value.longitude] };  if (Buffer.isBuffer(value)) return { $bytes: value.toString("base64") };  if (value instanceof DocumentReference) return { $ref: value.path };  if (Array.isArray(value)) return value.map(canonicalAdmin);  if (value && typeof value === "object") {    return Object.fromEntries(Object.keys(value).sort().map(k => [k, canonicalAdmin(value[k])]));  }  return value;}

4. Missing, undefined, null, and transforms are different states

JavaScript applications often blur undefined, missing object keys, and null. Firestore's stored document model does not. A client converter should make optionality explicit before calling the SDK. Do not silently turn “property absent” into null unless the schema contract says so; do not let undefined behavior vary by SDK/configuration. Server timestamps are write transforms whose local/pending client view can differ before acknowledgement, so tests must define whether they compare pre-ack local state or committed server state.

normalize-product.mjs · reject ambiguous client objects
export function normalizeProductInput(input) {  const out = {};  if (typeof input.name !== "string" || input.name.length === 0) throw new Error("name required");  out.name = input.name;  if (input.subtitle === null) out.subtitle = null;  else if (input.subtitle !== undefined) out.subtitle = String(input.subtitle);  // undefined => field omitted by AtlasMart policy  if (input.priceMinor !== undefined) {    if (!Number.isSafeInteger(input.priceMinor)) throw new Error("priceMinor must be a safe integer");    out.priceMinor = input.priceMinor;  }  return out;}
Server-only field rule

Client-writable objects must never accept fields such as internalRiskScore, audit actor, payment gateway raw payload, or server authorization decisions merely because Firestore would store them. Remove or reject them before client writes and enforce the trust boundary with Security Rules/server authorization.

5. Hands-on lab: one typed fixture, two SDK paths, one canonical result

The trusted Admin script writes the fixture. The Web script reads it through the client SDK. A second Admin read canonicalizes the same stored data. Both canonical objects should match except for fields intentionally hidden/forbidden by the client path. In a real mobile repository, Android/Apple tests add a third/fourth encoder with the same expected fixture.

scripts/ch02-schema-seed.mjs · trusted writer
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";process.env.GCLOUD_PROJECT = "demo-atlasmart-firestore";import { initializeApp } from "firebase-admin/app";import { getFirestore, Timestamp, GeoPoint } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();await db.doc("products/p-schema-001").set({  name: "Trail Camera",  priceMinor: 12990,  description: "Weatherproof 4K camera",  updatedAt: Timestamp.fromDate(new Date("2026-09-14T12:00:00.123Z")),  categoryRef: db.doc("categories/cameras"),  warehouse: new GeoPoint(47.3769, 8.5417),  checksum: Buffer.from("deadbeef", "hex"),  tags: ["camera", "outdoor"],  attributes: { resolution: "4K", weatherproof: true },  internalRiskScore: 0.02,  schemaVersion: 2});
scripts/ch02-schema-web-check.mjs · semantic assertions
const snap = await getDoc(doc(db, "products", "p-schema-001"));const data = snap.data();console.assert(snap.exists());console.assert(data.name === "Trail Camera");console.assert(Number.isSafeInteger(data.priceMinor));console.assert(data.updatedAt instanceof Timestamp);console.assert(data.categoryRef instanceof DocumentReference);console.assert(data.warehouse instanceof GeoPoint);console.assert(data.checksum instanceof Bytes);console.assert(Array.isArray(data.tags));console.assert(data.attributes.weatherproof === true);console.assert(data.schemaVersion === 2);console.log(JSON.stringify(canonicalWeb(data), null, 2));

If the Chapter 01 Rules still allow public product reads, the Web client can observe the full product document. That is deliberate for the synthetic lab, not a recommendation to expose internal server-only fields. Before production, either separate private fields into a server-only document/path or tighten the read model; Security Rules do not provide field-level redaction of a document read.

6. Index and schema contract belong in the same review

Serialization correctness is not enough. The Chapter 02 acceptance bundle should review firestore.indexes.json alongside the field contract so reviewers can ask whether each queryable field has the necessary index and whether long/dynamic fields are intentionally exempt. A schema change that turns a scalar into a large array can alter index entry counts even if every SDK still deserializes it successfully.

firestore.indexes.json · final Chapter 02 baseline
{  "indexes": [],  "fieldOverrides": [    {      "collectionGroup": "products",      "fieldPath": "description",      "indexes": []    },    {      "collectionGroup": "products",      "fieldPath": "attributes",      "indexes": []    },    {      "collectionGroup": "orders",      "fieldPath": "rawGatewayPayload",      "indexes": []    }  ]}

Future chapters may add indexes as query contracts emerge. They should not re-enable all automatic indexing merely because a query fails; the correct fix is a minimal index/model change justified by the access pattern.

7. Deliberately wrong approach: validate only the TypeScript interface

A TypeScript interface disappears at runtime and says nothing about Security Rules, native timestamp/reference wrappers, integer precision from another language, index fan-out, or a stale mobile client writing an older shape. If AtlasMart ships based only on static types in one repository, production data can drift while every local build remains green.

The repair is layered: runtime input validation, emulator rules tests, typed round-trip fixtures, canonical cross-SDK assertions, schemaVersion-compatible readers, index configuration in source control, and bounded production verification for behaviors the emulator cannot prove.

Chapter 02 acceptance decision

The schema is ready for Chapter 03 only when paths, value types, missing/null policy, ID strategy, growth limits, index intent, trust boundaries, and cross-SDK serialization are explicit enough that a new access pattern can be evaluated without guessing.

Knowledge check

Check your understanding

  1. Why should cross-SDK tests compare canonical semantic values rather than wrapper class names?
  2. Why is TypeScript alone insufficient as a Firestore schema?
  3. What is the safe policy for an external int64 identifier that must round-trip through JavaScript?
  4. Why can a product document be readable but still unsafe to expose to clients?
  5. What does Chapter 02 hand to Chapter 03?
Review the answers

1. Each SDK can expose different host-language wrapper classes for the same Firestore type. Path/seconds/coordinates/bytes are portable semantics; class names are not.

2. It provides compile-time structure in one codebase but does not enforce stored data, Security Rules, other SDKs, runtime inputs, index intent, or migrations.

3. Treat it as an opaque string unless the domain explicitly guarantees a safe integer range.

4. Firestore reads return the document allowed by Rules; sensitive server-only fields embedded in that same readable document are not magically redacted. Separate/tighten the data model.

5. A tested contract for paths, IDs, types, optionality, limits, index intent, security/trust ownership, and serialization. Chapter 03 can now optimize that contract for real access patterns.

Summary and next step

Chapter 02 turned Firestore's document model into an executable contract. AtlasMart now knows where documents live, which native values they contain, which limits/index rules constrain them, how IDs affect distribution and privacy, and how Web/mobile/server code must canonicalize the same stored semantics. The next chapter starts from screens, queries, and transactions and decides what to embed, reference, duplicate, or fan out.

Next: Start from Screens/Queries/Transactions—not Entity Diagrams—When Designing Firestore Models.

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.