Chapter 11 · Subcollections, Collection Groups, Hierarchies, Deletion, and Data Lifecycle

Deep Hierarchies vs Flat Collections: Queryability, Security, Ownership, and Operational Complexity

Model AtlasMart hierarchy from ownership, query, security and lifecycle requirements, comparing deep subcollections with flatter root collections.

Intermediate140–170 minutesHierarchy · ownership · lifecycleFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Design Firestore hierarchy from ownership, query and deletion boundaries instead of using nesting as visual organization alone.

02

Compare root collections and nested subcollections for queryability, Security Rules, lifecycle ownership and operational discovery.

03

Produce a path inventory that exposes every AtlasMart descendant before destructive work begins.

04

Recognize that a deep path can clarify ownership while simultaneously increasing rules, deletion and retention complexity.

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 11 reproducibility baseline · reviewed 16 September 2026

AtlasMart continues the same mandatory environment used in Chapters 01–10: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database, 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 with @google-cloud/firestore 9.1.0, and Node.js 22+. Mandatory deletion/lifecycle work remains local and isolated. Managed bulk delete, managed export/import, PITR, production IAM, Cloud Storage and legal/compliance procedures are discussed but are not falsely claimed to have run in the emulator.

Evidence boundary

The Emulator Suite is appropriate for proving path structure, document-delete versus surviving-subcollection behavior, collection-group query results, Security Rules behavior, recursive-cleanup logic and deterministic post-delete verification. It does not establish production delete throughput, billed read/delete cost, managed bulk-delete progress, export consistency, IAM behavior, backup/PITR recovery, legal-retention compliance or p95/p99 latency. Recursive deletion is multi-operation work rather than one atomic cascade; every production run needs a scoped target, project/database guard, durable audit evidence and post-delete verification.

1. The AtlasMart problem: hierarchy is an operational contract

AtlasMart has tenants, tenant users, orders and order messages. A relational instinct might flatten everything into root collections; a UI instinct might nest every object beneath its owner. Neither shape is automatically correct. In Firestore, a path decides which collection query is convenient, which Security Rules pattern matches, how lifecycle ownership is inferred, whether collection-group queries are required and how destructive cleanup must discover descendants.

A root collection begins directly under the database, such as orders/o-1001. A subcollection is a collection beneath a document, such as tenants/t-acme/orders/o-1001/messages/m-001. A collection group is every collection with the same collection ID, regardless of its parent path. A lifecycle owner is the application object whose retention/deletion policy governs a child; Firestore does not enforce that concept for you.

Shape Convenient operation Security/lifecycle benefit Operational cost
tenants/{tenant}/orders/{order} Read one tenant's orders Path encodes tenant ownership Cross-tenant analytics needs collection-group or duplication
orders/{order} Global order queries are simple Fewer path levels Tenant ownership must live in fields/rules and deletion manifests
.../orders/{order}/messages/{message} Messages naturally owned by order Narrow parent-scoped reads Parent deletion does not cascade; recursive cleanup required
messages/{message} Global messages easy to query Simple top-level index surface Ownership/lifecycle must be explicit fields; easy to forget copies

2. Start with journeys, then assign ownership

AtlasMart needs three primary journeys: a seller opens one tenant dashboard, a customer reads one order thread, and support searches recent messages for one tenant. The first two favor hierarchy. The third is still possible because all messages subcollections form a collection group. The path therefore preserves ownership without sacrificing the cross-parent query, provided indexes and rules are designed for that access pattern.

path contract
tenants/t-acme  users/u-1001  orders/o-1001    messages/m-001  orders/o-1002    messages/m-002userDirectory/u-1001      # deliberate root-level identity lookup copyactivity/a-001             # deliberate root-level audit/activity copy

The two root-level copies are intentional. Their presence means “delete tenant subtree” is not sufficient for either tenant deletion or data-subject deletion. Chapter 11 therefore treats deletion as a manifest-driven operation, not a single path operation.

3. Make the hierarchy observable before you delete anything

trusted emulator initialization
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, FieldValue, Timestamp } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();
seed deterministic AtlasMart hierarchy
async function seedHierarchy() {  const batch = db.batch();  batch.set(db.doc("tenants/t-acme"), {    name: "Acme Seller", lifecycleState: "ACTIVE", schemaVersion: 4  });  batch.set(db.doc("tenants/t-acme/users/u-1001"), {    tenantId: "t-acme", uid: "u-1001", displayName: "Ava", lifecycleState: "ACTIVE", schemaVersion: 4  });  batch.set(db.doc("tenants/t-acme/orders/o-1001"), {    tenantId: "t-acme", customerId: "u-1001", status: "PAID", totalMinor: 12990,    lifecycleState: "ACTIVE", schemaVersion: 4  });  batch.set(db.doc("tenants/t-acme/orders/o-1002"), {    tenantId: "t-acme", customerId: "u-1002", status: "PAID", totalMinor: 4950,    lifecycleState: "ACTIVE", schemaVersion: 4  });  batch.set(db.doc("tenants/t-acme/orders/o-1001/messages/m-001"), {    tenantId: "t-acme", orderId: "o-1001", authorId: "u-1001",    text: "Please leave at reception", sentAt: Timestamp.fromMillis(1789570800000), schemaVersion: 4  });  batch.set(db.doc("tenants/t-acme/orders/o-1002/messages/m-002"), {    tenantId: "t-acme", orderId: "o-1002", authorId: "u-1002",    text: "Thanks", sentAt: Timestamp.fromMillis(1789570860000), schemaVersion: 4  });  batch.set(db.doc("userDirectory/u-1001"), {    uid: "u-1001", tenantId: "t-acme", lifecycleState: "ACTIVE", schemaVersion: 4  });  batch.set(db.doc("activity/a-001"), {    subjectUid: "u-1001", tenantId: "t-acme", type: "ORDER_CREATED", schemaVersion: 4  });  await batch.commit();}
inventory documents and subcollections recursively
async function inventoryDocument(ref, out = []) {  const snap = await ref.get();  out.push({ path: ref.path, exists: snap.exists });  for (const coll of await ref.listCollections()) {    const children = await coll.get();    for (const child of children.docs) await inventoryDocument(child.ref, out);  }  return out;}async function inventoryCollection(ref, out = []) {  const snap = await ref.get();  for (const doc of snap.docs) await inventoryDocument(doc.ref, out);  return out;}
print a deterministic path inventory
await seedHierarchy();const paths = await inventoryDocument(db.doc("tenants/t-acme"));for (const p of paths.sort((a,b)=>a.path.localeCompare(b.path))) {  console.log(`${p.exists ? "DOC" : "MISSING"} ${p.path}`);}

The expected inventory contains the tenant, two orders, their messages and the tenant user. It does not contain userDirectory/u-1001 or activity/a-001, which is precisely why lifecycle design must record data outside the hierarchy.

4. Boundary: deep nesting is not recursive ownership enforcement

Firestore supports nested subcollections, but a descendant document can continue to exist even when its parent document is absent. A path therefore expresses association, not cascade semantics. Likewise, Security Rules that protect /tenants/{tenantId} do not magically apply the way an inheritance hierarchy might in an object-oriented language; every applicable path pattern must be correct.

Wrong approach: nest everything forever

Deeply nesting resources without a collection-group, index, rules and deletion plan creates data that is hard to query and easy to orphan. Repair the design by defining ownership and lifecycle for each collection ID, recording cross-tree copies, and building inventory/verification tooling before production data exists.

5. Standard vs Enterprise, Native vs MongoDB compatibility

The mandatory lab uses Standard Native Core operations. Enterprise Native Core keeps the same broad document/subcollection mental model but differs in indexing and billing, so query-cost assumptions must be re-measured. Enterprise Pipeline is an advanced query execution surface, not a replacement for lifecycle ownership. Firestore with MongoDB compatibility has a different driver/query surface; do not port Native Security Rules or client hierarchy assumptions into it without checking the compatibility documentation. Server/Admin code uses IAM in production and bypasses Native client Security Rules, so recursive cleanup belongs behind a trusted operational boundary.

6. Reproducible AtlasMart lab

package.json
{  "name": "atlasmart-firestore-ch11",  "private": true,  "type": "module",  "engines": { "node": ">=22" },  "dependencies": {    "firebase": "12.19.0",    "firebase-admin": "14.4.0"  },  "devDependencies": {    "firebase-tools": "15.30.0",    "@firebase/rules-unit-testing": "5.0.2"  }}
firebase.json
{  "firestore": {    "rules": "firestore.rules",    "indexes": "firestore.indexes.json"  },  "emulators": {    "firestore": { "port": 8080 },    "auth": { "port": 9099 },    "ui": { "enabled": true, "port": 4000 }  }}
firestore.rules
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    function signedIn() { return request.auth != null; }    function sameTenant(tenantId) {      return signedIn() && request.auth.token.tenantId == tenantId;    }    match /tenants/{tenantId} {      allow read: if sameTenant(tenantId);      allow write: if false;      match /users/{uid} {        allow read: if sameTenant(tenantId);        allow write: if sameTenant(tenantId) && request.auth.uid == uid;      }      match /orders/{orderId} {        allow read: if sameTenant(tenantId);        allow write: if false;        match /messages/{messageId} {          allow read: if sameTenant(tenantId);          allow write: if false;        }      }    }    // Required for collection-group queries over every messages collection.    match /{path=**}/messages/{messageId} {      allow read: if signedIn() && resource.data.tenantId == request.auth.token.tenantId;      allow write: if false;    }    match /deletionAudits/{id} { allow read, write: if false; }    match /retentionHolds/{id} { allow read, write: if false; }    match /{document=**} { allow read, write: if false; }  }}
firestore.indexes.json
{  "indexes": [    {      "collectionGroup": "messages",      "queryScope": "COLLECTION_GROUP",      "fields": [        { "fieldPath": "tenantId", "order": "ASCENDING" },        { "fieldPath": "sentAt", "order": "DESCENDING" }      ]    }  ],  "fieldOverrides": []}
local setup
mkdir atlasmart-firestore-ch11 && cd atlasmart-firestore-ch11npm init -ynpm install firebase@12.19.0 firebase-admin@14.4.0npm install --save-dev firebase-tools@15.30.0 @firebase/rules-unit-testing@5.0.2# Save firebase.json, firestore.rules and firestore.indexes.json from this lesson.npx firebase-tools@15.30.0 emulators:start --project demo-atlasmart-firestore --only firestore,auth

Seed the hierarchy, print the path inventory, then implement a competing flat model under flatOrders and flatMessages. For each design, write down the exact query needed for “all orders for t-acme,” “all messages for o-1001,” and “all recent messages for t-acme.” Do not benchmark emulator latency as production evidence; instead count queries, returned documents, rules surfaces and deletion targets.

Decision surface Nested model Flat model
Tenant-scoped ownership Encoded in path plus fields Field-only
One-order messages Direct subcollection Root query by orderId
All tenant messages Collection-group query Root query
Parent deletion Children survive unless recursively deleted No implicit relationship to children
Tenant purge discovery Tree + deliberate external copies Multiple root queries/manifest entries
Rules complexity Recursive/group rules plus parent ownership Field-based root rules

Production judgment

Prefer hierarchy when ownership and bounded parent-scoped access are strong, but only if every subcollection has a collection-group/index plan and a lifecycle plan. Prefer flatter structures when global querying dominates, but compensate with explicit ownership fields and deletion manifests. The deciding dimensions are query scope, rules proofability, delete discoverability, retention boundaries, future analytics and operator tooling—not aesthetic preference.

Lesson 2 drills into collection-group queries, where one collection ID becomes a cross-parent query surface and a security boundary.

Knowledge check

  1. Does a nested path guarantee a child is deleted with its parent?
  2. What does a collection group contain?
  3. Why record root-level copies such as userDirectory?
  4. What should be inventoried before destructive cleanup?
  5. What does the emulator prove here?
Review the answers

1. No. Document deletion does not cascade to subcollection documents.

2. All collections with the same collection ID at any hierarchy level.

3. Because deleting only the tenant tree would otherwise leave duplicated subject/tenant data behind.

4. The target tree, deliberate external copies, collection-group copies, retention holds and expected post-delete exceptions.

5. Path, query, rules and cleanup logic—not production latency, billing, IAM or legal compliance.

Summary and next step

Hierarchy is simultaneously a query structure, security structure and lifecycle structure. AtlasMart now has an explicit ownership map and path inventory. Next we make collection-group queries safe across those parent paths.

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.