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.
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.
Define a canonical AtlasMart schema contract for paths, types, optional/null policy, indexing, and writer trust level.
Compare Web/mobile/server representations of timestamps, references, GeoPoints, bytes, numbers, arrays, and maps.
Build round-trip assertions that canonicalize SDK wrapper types before comparison.
Detect dangerous serialization drift such as int64 precision loss, Date/string coercion, undefined fields, and server-only fields entering client writes.
Produce a Chapter 02 acceptance checklist that bridges cleanly into access-pattern-driven modeling in Chapter 03.
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.
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.
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;}
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.
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;}
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.
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});
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.
{ "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.
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
- Why should cross-SDK tests compare canonical semantic values rather than wrapper class names?
- Why is TypeScript alone insufficient as a Firestore schema?
- What is the safe policy for an external int64 identifier that must round-trip through JavaScript?
- Why can a product document be readable but still unsafe to expose to clients?
- 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
- Cloud Firestore data model — Official document/collection hierarchy and subcollection model.
- Choose a data structure — Official guidance for nested data, subcollections, and root-level collections.
- Supported data types — Current Native data types, sort ordering, precision, and edition-specific value behavior.
- Usage and limits — Current document, field, path, nesting, index-entry, and request limits.
- Index types in Cloud Firestore — Automatic/manual indexing, map/array indexing, index entries, and exemptions.
- Best practices for Cloud Firestore — Official document-ID, hotspot, location, and index-fan-out guidance.
- Add data to Cloud Firestore — Current SDK examples for IDs, timestamps, nested fields, and custom object conversion.
- Firebase release notes — Current Firebase SDK and CLI version baseline.
- Connect to the Firestore Emulator — Local emulator connection and production-difference guidance.