Chapter 05 · Queries: Filters, Ordering, Limits, Cursors, Collection Groups, and Query Constraints

Collection Group Queries Across Subcollections and Their Modeling / Security Implications

Query Firestore collection groups safely across subcollections while aligning data modeling, index scope, Security Rules, ownership, and lifecycle.

Intermediate110–130 minutesCollection-group + Rules contractFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

AtlasMart stores each user's orders under users/{uid}/orders/{orderId}. A support dashboard now needs “all paid orders” across users. That access pattern is exactly what a collection-group query can express, but the collection-group name, index scope, duplicated ownership fields, and Rules pattern become part of the design.

01

Explain collection-group scope as all collections with the same collection ID, regardless of parent path.

02

Distinguish a collection-scoped index from a collection-group-scoped index.

03

Write Security Rules v2 patterns that can authorize a collection-group query and explain why Rules still are not filters.

04

Denormalize ownership/tenant fields into child documents when that makes the query and authorization predicate provable.

05

Account for lifecycle: deleting a parent document does not automatically delete its subcollection documents.

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 05 reproducibility baseline · reviewed 15 September 2026

AtlasMart keeps the same local course environment used in Chapters 01–04: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database for Core-operation labs, 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 (which currently carries @google-cloud/firestore 9.1.0), @firebase/rules-unit-testing 5.0.2, and Node.js 22+. No moving latest tag is required by the lab.

What was and was not executed while authoring

The generation environment does not run the Firebase emulators, so code and commands are checked against current official Firebase/Google Cloud documentation but the output shown in this lesson is an expected invariant, not fabricated captured output. In particular, the Firestore emulator does not enforce production composite indexes in the same way as the managed service. Operator validation, deterministic result ordering, cursor contracts, and Security Rules tests belong in the mandatory local lab; a missing-composite-index error or Query Explain evidence must be treated as a separately labeled production verification.

1. Collection group means same collection ID across the hierarchy

collectionGroup(db, "orders") addresses every orders collection in the database, including users/u-alice/orders and users/u-bob/orders. It does not mean “join parent users with child orders.” The returned documents are still order documents. If the query needs ownerUid or tenantId for filtering/authorization, those fields should normally exist on each order document rather than being assumed from an inaccessible parent join.

Web modular SDK · collection-group query
import { collectionGroup, getDocs, orderBy, query, where } from "firebase/firestore";const paid = query(  collectionGroup(db, "orders"),  where("ownerUid", "==", currentUid),  where("status", "==", "paid"),  orderBy("createdAt", "desc"));const snap = await getDocs(paid);console.log(snap.docs.map(d => ({ path:d.ref.path, ...d.data() })));

2. Query scope and index scope must agree

Automatic collection indexes are not the same as collection-group indexes. A filtered or ordered collection-group query needs an index with collection-group scope corresponding to its fields/order. The emulator can execute many query shapes without reproducing production composite-index enforcement, so a local success does not prove the managed index exists.

Question Single collection Collection group
Scope One concrete collection path All collections sharing an ID
Typical index scope Collection Collection group
Rules match Concrete/recursive path as designed Rules v2 recursive wildcard pattern is typically required
Parent data Known from query path Not automatically joined into each result
Lifecycle Parent/document policy local to path Must account for orphaned descendants across the whole group

3. Rules must prove the whole candidate set

For collection-group queries, use rules_version = '2' and a recursive wildcard pattern that matches the target collection. A robust AtlasMart rule can authorize an order when the authenticated UID equals the duplicated ownerUid. The client query must include constraints compatible with that proof. A broad “all orders” query does not become safe merely because each returned document could be checked individually.

firestore.rules · owner-scoped collection-group rule
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /{path=**}/orders/{orderId} {      allow get: if request.auth != null && request.auth.uid == resource.data.ownerUid;      allow list: if request.auth != null && request.auth.uid == resource.data.ownerUid;    }  }}
Rules are not filters.

A query for every order cannot rely on the rule to remove Bob's documents from Alice's response. The query must constrain the possible result set so the Rules engine can prove it is safe. Chapter 12–13 will deepen the exact rule/query proof model; here the modeling consequence is the key point.

4. Deliberately wrong approach: infer owner from the path after reading

A developer issues an unrestricted collection-group query, then parses users/u-alice from each path in JavaScript and discards other users. This is both an authorization error and unnecessary read amplification. The corrected model duplicates ownerUid (and, where appropriate, tenantId) into the order document, constrains the query on that field, and validates those fields on writes so clients cannot forge ownership.

Duplication creates a maintenance obligation: the copied field should be immutable for the order's lifetime or changed only through a trusted, tested migration. Chapter 03's denormalization rule still applies—duplicate deliberately, with a consistency policy.

5. Hands-on lab: group query plus rule contract

shell · pinned local dependencies
mkdir atlasmart-firestore-query-labcd atlasmart-firestore-query-labnpm 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.2node --versionnpx firebase --versionnpx firebase emulators:start --only firestore,auth --project demo-atlasmart-firestore
seed-query-fixtures.mjs · deterministic AtlasMart query data
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";import { initializeApp, applicationDefault } from "firebase-admin/app";import { getFirestore, Timestamp } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();const catalog = {  "p-1001": { category:"camera",  price:99,  rating:4.8, tags:["outdoor","camera"], published:true,  rank:10, sellerId:"seller-a" },  "p-1002": { category:"camera",  price:99,  rating:4.5, tags:["studio","camera"],  published:true,  rank:20, sellerId:"seller-a" },  "p-1003": { category:"sensor",  price:49,  rating:4.8, tags:["outdoor","iot"],     published:true,           sellerId:"seller-b" },  "p-1004": { category:"sensor",  price:149, rating:4.1, tags:["industrial","iot"], published:false, rank:null, sellerId:"seller-b" },  "p-1005": { category:"camera",  price:199, rating:4.9, tags:["outdoor","camera"], published:true,  rank:30, sellerId:"seller-c" },  "p-1006": { category:"gateway", price:99,  rating:4.2, tags:["iot"],              published:true,  rank:40, sellerId:"seller-c" }};for (const [id, data] of Object.entries(catalog)) {  await db.collection("catalogItems").doc(id).set({ ...data, updatedAt: Timestamp.fromMillis(1760000000000) });}const orders = [  ["u-alice","o-1001",{ ownerUid:"u-alice", tenantId:"seller-a", status:"paid",    total:198, createdAt:Timestamp.fromMillis(1760000100000) }],  ["u-alice","o-1002",{ ownerUid:"u-alice", tenantId:"seller-b", status:"shipped", total:49,  createdAt:Timestamp.fromMillis(1760000200000) }],  ["u-bob",  "o-1003",{ ownerUid:"u-bob",   tenantId:"seller-a", status:"paid",    total:99,  createdAt:Timestamp.fromMillis(1760000300000) }]];for (const [uid,id,data] of orders) await db.doc(`users/${uid}/orders/${id}`).set(data);console.log("seeded", Object.keys(catalog).length, "catalog items and", orders.length, "orders");
collection-group-contract.mjs · server-side fixture proof
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";import assert from "node:assert/strict";import { initializeApp } from "firebase-admin/app";import { getFirestore } from "firebase-admin/firestore";initializeApp({projectId:"demo-atlasmart-firestore"});const db=getFirestore();const snap=await db.collectionGroup("orders")  .where("tenantId","==","seller-a")  .orderBy("createdAt","asc").get();assert.deepEqual(snap.docs.map(d=>d.ref.path),[  "users/u-alice/orders/o-1001",  "users/u-bob/orders/o-1003"]);console.log("collection-group server contract passed");

Then use @firebase/rules-unit-testing@5.0.2 to create an authenticated Alice test context and assert that an owner-constrained group query succeeds while a broad group query or Bob-targeting query is denied. Keep the Admin seed outside Rules evaluation; Admin/server SDK access is privileged and proves database state, not client authorization.

Lifecycle test.

Delete users/u-alice in the emulator without recursively deleting its descendants. Verify that users/u-alice/orders/o-1001 can still exist. That observation is why account-erasure workflows must enumerate/verify nested data rather than assuming parent deletion cascades.

Production judgment

Collection groups are powerful when the same child concept must be queried globally, but that naming decision becomes an API surface. Reusing a generic subcollection ID such as events for unrelated semantics can accidentally create one enormous group with awkward Rules and index requirements. Name subcollections according to durable meaning and plan deletion/retention across descendants.

Knowledge check

  1. What does collectionGroup(db,"orders") include?
  2. Why copy ownerUid into an order document?
  3. Why can an emulator-successful group query still fail in production?
  4. Why is a broad collection-group query followed by client filtering unsafe?
  5. What lifecycle fact must deletion code remember?
Review the answers

1. All collections named orders anywhere in the database hierarchy, not parent documents or an implicit join.

2. It can make the collection-group query and Rules predicate explicit/provable without relying on a server-side parent join.

3. The emulator does not reproduce all production composite-index enforcement; production may require a collection-group index.

4. Rules are not filters; authorization must be provable from the query candidate set.

5. Deleting a parent document does not automatically delete its subcollection documents.

Summary and next step

Collection-group scope ties modeling, indexes, and authorization together. Lesson 4 generalizes that idea: when a desired query is unsupported, ambiguous because of missing fields, or dependent on an absent production index, change the access pattern or data shape rather than hiding the mismatch.

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.