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

Collection Group Queries and Index / Rules Requirements Across Parent Documents

Secure and index AtlasMart collection-group queries across parent documents while proving tenant isolation and query-rule compatibility.

Intermediate135–165 minutesCollection groups · rules · indexesFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Explain exactly how a collection-group query crosses parent documents and why identical collection IDs become one query surface.

02

Write rules_version 2 Security Rules that authorize the collection group itself rather than assuming parent collection rules act as filters.

03

Derive a collection-group composite index from the AtlasMart tenant-message query contract.

04

Validate query results, tenant isolation and missing-index/rules boundaries without overclaiming emulator equivalence to production.

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: support needs messages across many orders

Messages are stored under each order because the order owns their lifecycle. Support nevertheless needs “the 20 newest messages for tenant t-acme.” Reading every order and then every messages subcollection would create read amplification and complex client logic. A collection-group query targets every collection named messages, then applies normal filters/order/limits.

Web modular collection-group query
import { collectionGroup, query, where, orderBy, limit, getDocs } from "firebase/firestore";const q = query(  collectionGroup(db, "messages"),  where("tenantId", "==", "t-acme"),  orderBy("sentAt", "desc"),  limit(20));const snap = await getDocs(q);console.table(snap.docs.map(d => ({ path: d.ref.path, ...d.data() })));

The query does not care whether a matching messages collection sits under an order, a support case, or some future path. That is useful and dangerous: reusing the same collection ID elsewhere automatically expands the collection group.

2. Security Rules must secure the group, not post-filter it

Security Rules are not row filters. Firestore evaluates whether a query could return forbidden documents; it does not fetch everything and remove unauthorized documents afterward. Collection-group queries require Security Rules version 2 recursive-wildcard behavior and an explicit rule for the group.

collection-group rule
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /{path=**}/messages/{messageId} {      allow read: if request.auth != null                  && resource.data.tenantId == request.auth.token.tenantId;      allow write: if false;    }  }}

The AtlasMart query includes where("tenantId", "==", signed-in tenant) so the query contract is compatible with the rule. A broad query over all messages cannot rely on the rule to hide another tenant’s documents.

Wrong approach: broad query + client filtering

Fetching a broad collection group and filtering tenant IDs in application code is both a security design error and unnecessary read amplification. Repair it by making the authorized tenant constraint part of the server-evaluated query and by testing the rule against adversarial broad queries.

3. Index the query contract

firestore.indexes.json
{  "indexes": [    {      "collectionGroup": "messages",      "queryScope": "COLLECTION_GROUP",      "fields": [        { "fieldPath": "tenantId", "order": "ASCENDING" },        { "fieldPath": "sentAt", "order": "DESCENDING" }      ]    }  ],  "fieldOverrides": []}

In Standard Native mode, compound collection-group queries can require an appropriate collection-group index. Production can return a missing-index error with a creation path; the emulator should not be treated as proof that production has the same index state. Keep the index definition in source control and deploy it deliberately.

4. Make cross-parent results observable

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();}
trusted result contract
await seedHierarchy();const snap = await db.collectionGroup("messages")  .where("tenantId", "==", "t-acme")  .orderBy("sentAt", "desc")  .limit(20)  .get();const paths = snap.docs.map(d => d.ref.path);console.log(paths);if (paths.length !== 2) throw new Error(`expected 2 t-acme messages, got ${paths.length}`);if (!paths.every(p => p.includes("/messages/"))) throw new Error("unexpected path");

The trusted Admin query proves the collection-group result shape. A separate client/rules test must prove authorization. Do not confuse the two: Admin/server libraries bypass client Security Rules in production and use IAM.

5. Adversarial rules test

rules-unit-testing sketch
import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing";import { collectionGroup, query, where, orderBy, getDocs } from "firebase/firestore";const env = await initializeTestEnvironment({  projectId: "demo-atlasmart-firestore",  firestore: { rules: /* load firestore.rules text */ rulesText }});const acme = env.authenticatedContext("u-1001", { tenantId: "t-acme" }).firestore();await assertSucceeds(getDocs(query(  collectionGroup(acme, "messages"),  where("tenantId", "==", "t-acme"),  orderBy("sentAt", "desc"))));await assertFails(getDocs(query(  collectionGroup(acme, "messages"),  orderBy("sentAt", "desc"))));

The second query intentionally omits the tenant constraint. Its denial demonstrates that rules are evaluated against the query’s possible result set. A passing emulator test is necessary regression evidence, not proof of production IAM, billing or index-build readiness.

6. Collection ID is a security namespace

If AtlasMart later creates supportCases/{id}/messages/{id}, those documents join the same messages collection group. If their security semantics differ, either make the shared group rule safe for both, include discriminating fields that the query/rules can prove, or choose a different collection ID. A collection ID is therefore not just a naming preference—it can become a cross-hierarchy query and authorization namespace.

Risk surface Observable failure Design response
['Reused collection ID', 'Unexpected documents enter collection-group results', 'Use a distinct ID or add provable discriminator fields'] ['Missing tenant filter', 'Client query is denied by rules', 'Put authorization constraint into the query itself'] ['Missing composite group index', 'Production query returns missing-index failure', 'Keep COLLECTION_GROUP index in source control and deploy it'] ['Admin query assumed to test Rules', 'Trusted query succeeds despite bad client rule', 'Add explicit rules-unit-testing coverage']

Production judgment

Use collection groups when ownership belongs under parents but cross-parent access is a first-class requirement. Evaluate the design through index count, rules maintainability, tenant/subject isolation, collection-ID reuse risk, query selectivity and delete discoverability. Enterprise Native indexing differs from Standard, so repeat plan/cost reasoning there; Pipeline capabilities should not be assumed to share client Security Rules or realtime/offline surfaces. MongoDB compatibility has its own query/authorization model.

Lesson 3 now tests the lifecycle consequence: deleting an order document does not remove its messages descendants.

Knowledge check

  1. Why can a collection-group query see messages under different orders?
  2. Why must the query include the tenant constraint?
  3. What Security Rules version is required for the recursive wildcard collection-group pattern?
  4. What can change if a new hierarchy reuses the messages collection ID?
  5. Does a successful emulator query prove the production composite index is ready?
Review the answers

1. Because all subcollections with the ID messages belong to the same collection group.

2. Rules are not filters; the query must be provably compatible with the authorization condition.

3. rules_version 2.

4. Those documents join the same collection group and may fall under the same group rule/index surface.

5. No. Production index state/build status must be verified separately.

Summary and next step

Collection groups recover cross-parent queryability without flattening ownership, but they create an explicit rules/index namespace. Next we deliberately delete a parent and observe the orphaned descendants that remain.

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.