Chapter 04 · CRUD with Client and Server SDKs: Reads, Writes, Updates, Deletes, Preconditions, and Converters

Get Documents, Create / Set / Merge, Update, Delete, and Understand Missing vs Null Fields

Make Firestore CRUD semantics explicit: distinguish missing documents, missing fields, null values, overwrite versus merge versus update, conditional creation, field deletion, and lifecycle-safe document deletion.

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

Learning outcomes

AtlasMart now has a stable access-pattern model, but CRUD bugs can still destroy that model. A profile patch can accidentally erase preferences, an update can fail because the document never existed, a nullable field can be confused with an absent field, and a server delete can leave orphaned subcollection data. This lesson treats every write method as a state transition with explicit preconditions rather than as interchangeable vocabulary.

01

Distinguish a missing document, an existing document with a missing field, and an existing field whose value is null.

02

Predict the state effect of get, create, set/overwrite, set/merge, update, field deletion, and document deletion.

03

Use server-side create/update-time preconditions when a write must fail rather than silently overwrite concurrent state.

04

Explain why Web-client authorization and trusted-server authorization are separate controls even when they target the same path.

05

Prove CRUD results with before/after snapshots, update times, rule failures, and lifecycle-aware cleanup.

Chapter 04 baseline reviewed 15 September 2026

The lab continues Chapters 01–03 without changing the environment: 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 (which currently carries @google-cloud/firestore 9.1.0); Node.js 22 or newer. The default teaching surface is Firestore Standard edition, Native mode, Core operations, default database (default), emulator-first. Enterprise/Pipeline/MongoDB-compatibility behavior is labeled rather than generalized.

Trust, execution, and evidence note

Browser/mobile SDK requests are untrusted requests evaluated by Firebase Authentication context plus Cloud Firestore Security Rules. Admin/server client libraries are privileged and bypass Firestore Security Rules; production access is governed by IAM/ADC and by authorization logic in your server application. The emulator demonstrates these trust paths and CRUD semantics, but it does not prove production IAM, regional latency, billing, quotas, contention, or service availability. Expected failures are described by invariant/error family rather than fabricated captured output.

1. Three different absences: document missing, field missing, field null

Firestore does not have one universal “empty” state. A missing document means a document reference names a location for which no document exists; a document snapshot reports that state separately from its data. An absent field means the document exists but the field path is not present. A null field is present and stores the Firestore null value. Those distinctions affect queries, converters, validation, patch semantics, and product meaning.

State Snapshot interpretation AtlasMart meaning Do not confuse with
Document missing exists() / exists is false User/profile/product has no document at that path Existing object with empty fields
Field missing Document exists; key/path absent from data Feature never set, older schema, or intentionally deleted Explicit “none”
Field = null Key is present with null value Explicit no-value state if the contract permits it Unknown/unmigrated field

For AtlasMart, phoneNumber: null can mean “the user explicitly has no phone number,” while a missing phoneNumber can mean “this v1 profile predates the field.” If a converter collapses both to JavaScript undefined, the application loses migration information. Conversely, writing JavaScript undefined is not a portable Firestore data contract; represent optionality deliberately.

2. Get, create, set, merge, and update have different contracts

A read returns a snapshot, not proof that the document exists. A full set is an upsert-like replacement at a known path: if the document is absent it is created; if present its document fields are replaced by the supplied document unless merge semantics are requested. set(..., { merge: true }) preserves omitted fields, but an explicitly supplied map can still replace nested map content depending on the write shape. update patches named fields and fails if the target document does not exist. Server libraries additionally expose create(), which fails if the target already exists.

Operation Missing target Existing target Good use Typical failure
Web getDoc / server get Snapshot says missing Returns snapshot data Read current state Caller forgets existence check
set, no merge Creates Replaces supplied document state Known complete canonical representation Blind overwrite loses fields
set + merge Creates with supplied fields Merges supplied paths Upsert a partial projection/config Assuming every nested map update is path-granular
update Fails Patches named field paths Existing-object patch Missing document or stale assumptions
Server create Creates Fails Create-if-absent/idempotency record Retry interpreted as unexpected error
Web modular SDK · observe overwrite, merge, and missing-update behavior
import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";const ref = doc(db, "profiles", "u-alice");await setDoc(ref, { displayName: "Alice", locale: "en", schemaVersion: 1 });console.log("seed", (await getDoc(ref)).data());await setDoc(ref, { locale: "fa", schemaVersion: 1 }, { merge: true });console.log("merge", (await getDoc(ref)).data());await updateDoc(ref, { "preferences.theme": "dark" });console.log("patch", (await getDoc(ref)).data());// Deliberate failure: update requires the target document to exist.await updateDoc(doc(db, "profiles", "u-missing"), { locale: "en" });

3. Field deletion and document deletion are not the same lifecycle operation

Deleting one field uses a field-deletion sentinel in an update. That removes the field path while preserving the document. Deleting a document removes that document, but it does not recursively delete documents in subcollections. A path like profiles/u-alice/private/audit can remain reachable after profiles/u-alice is deleted. Therefore “delete profile” is a product/lifecycle workflow, not just one deleteDoc() call.

Web modular SDK · explicit null, missing field, and document delete
import { deleteDoc, deleteField, doc, setDoc, updateDoc } from "firebase/firestore";const ref = doc(db, "profiles", "u-alice");await setDoc(ref, { displayName: "Alice", phoneNumber: null, legacyAlias: "alice-1" });await updateDoc(ref, { legacyAlias: deleteField() }); // field becomes absentawait deleteDoc(ref);                                // document becomes absent// Any subcollection documents under profiles/u-alice are separate documents.// Recursive lifecycle cleanup must be designed and executed deliberately.
Deletion acceptance criterion

Before destructive production code, list every owned root document, subcollection, duplicated projection, search/vector copy, event record, backup/retention obligation, and legal-hold exception. This chapter uses only disposable emulator paths and does not present a client-side recursive delete as a production pattern.

4. Preconditions turn “last writer wins” into an explicit conflict

When a trusted service must change a document only if it has not changed since a prior read, use a conditional write rather than “read, compare in JavaScript, then blindly update.” Firestore's precondition model can require existence/nonexistence or a matching update time. In the Node server library, a document snapshot exposes updateTime; update and delete accept a precondition. This is useful for optimistic concurrency on a single document when a full transaction is unnecessary.

Trusted Node service · create-if-absent and update-time precondition
import { initializeApp } from "firebase-admin/app";import { getFirestore } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();const productRef = db.doc("products/p-1001");// create() succeeds only if the document does not already exist.await productRef.create({ name: "Trail Camera", priceCents: 12990, schemaVersion: 1 });const before = await productRef.get();const observedUpdateTime = before.updateTime;await productRef.update(  { priceCents: 12490 },  { lastUpdateTime: observedUpdateTime });// If another writer modifies the document after the read, the same precondition fails.

A precondition does not authorize the business action. The Admin/server path is privileged, so the service must still decide whether the authenticated caller is allowed to change the price. A successful update-time match proves freshness relative to one observed version, not that the price itself is valid.

5. Hands-on lab: CRUD state matrix

Continue the same local AtlasMart workspace. Keep firebase emulators:start --only auth,firestore running. Admin/server examples set FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 and GCLOUD_PROJECT=demo-atlasmart-firestore; browser examples connect the modular Web SDK explicitly to the Firestore/Auth emulators. Use only synthetic Chapter 04 documents such as profiles/u-alice, products/p-1001, crudOperations/<operationId>, and named test users. Cleanup must target only those paths.

lab matrix · record state, result, and evidence
Case A: GET missing profiles/u-missing -> snapshot missing; no fabricated empty objectCase B: SET full profile -> document exists with exact canonical fieldsCase C: SET merge locale -> displayName remains; locale changesCase D: UPDATE nested field -> target must exist; only named path changesCase E: store phoneNumber=null -> field is present and explicitly nullCase F: deleteField(phoneNumber) -> field absent, document still existsCase G: Admin create same product twice -> first creates; duplicate create failsCase H: Admin update with stale lastUpdateTime -> conditional write failsCase I: delete profile -> parent absent; prove any seeded subcollection doc is separate

For each case, capture the document path, client/server identity, operation name, expected before state, expected after state, and error category. Do not assert exact emulator wording as a timeless API; assert the semantic outcome. Then reset only the Chapter 04 fixtures.

6. Deliberately wrong approach: blind replacement as “update”

Suppose the UI edits only locale but calls full setDoc(ref, { locale: "fa" }). The write can erase displayName, preferences, schemaVersion, and any server-managed fields because the code supplied a replacement document. The fix is not “always use merge.” The fix is to choose the operation that matches the contract: full set only when the caller owns the complete canonical representation; merge/update for controlled patches; server-side conditional writes or transactions for read-dependent invariants.

The same discipline applies to delete. “The document disappeared” is not enough evidence for a privacy/lifecycle requirement if subcollections, projections, exports, or backups remain.

Knowledge check

Check your understanding

  1. How is a missing field different from a field whose value is null?
  2. Why can a plain full set be dangerous for a partial UI edit?
  3. What is the useful semantic difference between server create() and set()?
  4. What does a lastUpdateTime precondition protect against?
  5. Why does deleting a parent document not prove its subcollection data is gone?
Review the answers

1. A missing field has no key/path in the document; null is an explicit stored value. They can carry different migration and product meaning.

2. A full set can replace document fields that the partial UI never loaded or intended to change. Use a patch/merge contract or submit a complete canonical object intentionally.

3. create() is conditional create-if-absent and fails when the document already exists; set() can create or overwrite depending on options.

4. It rejects the write if the target changed since the caller observed that update time, exposing a concurrent edit instead of silently overwriting it.

5. Firestore documents in subcollections are independent documents; parent document deletion is shallow with respect to subcollection documents.

Summary and next step

CRUD method names now map to explicit state transitions: snapshots prove existence, null is not absence, full set is not a patch, update requires an existing target, field deletion is not document deletion, and server preconditions can surface concurrent changes. Next, apply the same precision to nested field paths and atomic transforms.

Next: Field Paths, Nested Updates, Array Operations, Increment, Server Timestamps, and Atomic Field Transforms.

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.