Chapter 12 · Security Rules Foundations: Authentication Context, Match Paths, Reads/Writes, and Validation

Path Matching, Wildcards, Recursive Wildcards, get / exists, and Access-Call Limits

Trace AtlasMart path matching, recursive wildcards and bounded cross-document authorization with explicit access-call evidence.

Intermediate145–175 minutesPaths · wildcards · access callsFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0 · Rules tests 5.0.2Last reviewed: September 2026

Learning outcomes

01

Predict which document paths a nested match, single-segment wildcard or recursive wildcard covers under Rules v2.

02

Use get(), exists() and getAfter() only when cross-document policy is worth the read, complexity and evaluation limits.

03

Explain the 10-call and 20-call document-access limits and how per-operation limits still constrain batches/transactions.

04

Detect dangerous overlap where a broad matching allow silently grants access that a narrower rule appears to deny.

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

AtlasMart continues the same mandatory environment used in Chapters 01–11: 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, @firebase/rules-unit-testing 5.0.2, and Node.js 22+. Mandatory work is local and no-cost. The canonical lab uses Standard Native Core operations; Enterprise Native Core/Pipeline differences are labeled explicitly rather than silently mixed into Standard semantics.

Security evidence boundary

The Emulator Suite can prove deterministic allow/deny behavior for the rules source loaded into that emulator, mock Firebase Authentication claims, query/rule compatibility, field validation, wildcard matching and many access-call cases. It does not prove production IAM, App Check enforcement, production billing, latency, index deployment state, abuse resistance, or every backend/runtime difference. Mobile/web client requests are evaluated by Security Rules. Admin/server client libraries bypass Security Rules and are authorized by IAM; a passing client-rules test therefore says nothing about privileged server authorization.

1. The AtlasMart problem: path shape becomes policy shape

AtlasMart’s model contains profiles/{uid}, orders/{orderId} and products/{productId}/reviews/{reviewId}. A parent rule does not automatically protect all descendants. If the team assumes that a rule on /products/{productId} also covers /products/{productId}/reviews/{reviewId}, it can accidentally leave the subcollection unmatched/denied or later add a dangerous broad wildcard. Security review therefore starts with an explicit path inventory.

Matcher Matches Does not imply
/profiles/{uid} Exactly one document depth under profiles Any subcollection below that profile
/products/{productId}/reviews/{reviewId} Review documents under each product The product document itself
/{path=**}/reviews/{reviewId} in v2 Review documents at arbitrary ancestor depth / collection-group shape Authorization by itself
/{document=**} All document paths at any depth A reason to allow them

2. Nested matches are relative; rules do not cascade

equivalent nested and absolute shapes
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /products/{productId} {      allow get: if true;      match /reviews/{reviewId} {        allow get: if resource.data.visibility == "public";      }    }  }}// The inner match is equivalent to:// match /products/{productId}/reviews/{reviewId} { ... }

The outer product rule does not flow into the review subcollection. Every document read must match an allow rule on that document path.

Wrong approach: “the parent is private, therefore every child is private”

Firestore Security Rules match document paths, not ownership inheritance. Write explicit nested/collection-group rules and test direct access to descendants.

3. Recursive wildcard semantics changed in rules_version = '2'

Rules v2 makes {name=**} match zero or more path segments, while v1 uses one or more. Version 2 also permits the recursive wildcard in more positions and is required for collection-group query rules. This makes broad recursive patterns powerful and dangerous: overlapping allow statements are OR-like—if any matching allow condition is true, the request is allowed.

collection-group rule shape
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /{path=**}/reviews/{reviewId} {      allow get, list: if resource.data.visibility == "public";    }  }}
Wrong overlap

A narrow allow read: if false does not override another matching recursive rule that evaluates true. There is no “deny wins” rule-combining model. Audit all overlapping match statements before adding a broad recursive grant.

4. Cross-document policy with exists(), get() and getAfter()

Sometimes authorization depends on a separate membership/role document. exists() asks whether a fully-specified document exists. get() retrieves its fields for rule evaluation. getAfter() can inspect state after a transaction/batch but before commit so multiple writes can be required to move together. These calls are not free policy lookups: in production they can cause billed document reads even when a request is rejected, and they count against evaluation limits unless cached.

membership helper
function membershipPath(tenantId) {  return /databases/$(database)/documents/tenants/$(tenantId)/members/$(request.auth.uid);}function activeMember(tenantId) {  return request.auth != null    && exists(membershipPath(tenantId))    && get(membershipPath(tenantId)).data.state == "ACTIVE";}

Path variables inside document-access calls need $(variable) escaping. Prefer claims or data colocated on the protected document when that gives an equally trustworthy, simpler policy; use cross-document reads when the centralized policy value is truly required.

5. Document-access call limits are correctness limits

Request shape Maximum document access calls Extra constraint
Single document get/write 10 Cached calls may not count
Query request 10 Rules must also prove the whole result set
Multi-document read 20 Per-operation constraints still matter
Transaction 20 total Each operation still has its own 10-call limit
Batched write 20 total Each write still has its own 10-call limit

Exceeding the limit returns permission denied, which means a ruleset can become functionally incorrect as a workflow grows. Do not design “authorization joins” that traverse many documents. Rules functions have their own syntax/stack/expression/size limits as well.

bounded rule design sketch
BAD: for an order, look up customer -> tenant -> plan -> region -> feature -> policy -> ...BETTER: one authoritative membership/policy document, or trusted claims for stable role data,        plus fields on the protected document that let the query prove tenant/owner constraints.

6. Emulator exercise: prove matcher and access-call behavior

package.json
{  "name": "atlasmart-firestore-ch12",  "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 }  }}
local setup
mkdir atlasmart-firestore-ch12 && cd atlasmart-firestore-ch12npm 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 the lesson.npx firebase-tools@15.30.0 emulators:start --project demo-atlasmart-firestore --only firestore,auth
focused rules variant
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    function member(tenantId) {      return request.auth != null        && exists(/databases/$(database)/documents/tenants/$(tenantId)/members/$(request.auth.uid));    }    match /tenants/{tenantId}/orders/{orderId} {      allow get: if member(tenantId);      allow list: if member(tenantId) && resource.data.tenantId == tenantId;    }    match /{path=**}/reviews/{reviewId} {      allow get, list: if resource.data.visibility == "public";    }    match /{document=**} { allow read, write: if false; }  }}

Seed one membership, one order and nested public/private reviews with withSecurityRulesDisabled. Assert the member can get the order, a nonmember cannot, the public review succeeds, and the private review fails. Then deliberately replace member() with a chain of unique get() calls that exceeds the documented call budget and assert permission denied. Restore the bounded version before continuing.

test skeleton
const memberDb = testEnv.authenticatedContext("u-1001", { tenantId:"t-acme" }).firestore();const outsiderDb = testEnv.authenticatedContext("u-9000", { tenantId:"t-acme" }).firestore();await assertSucceeds(getDoc(doc(memberDb, "tenants/t-acme/orders/o-1001")));await assertFails(getDoc(doc(outsiderDb, "tenants/t-acme/orders/o-1001")));await assertSucceeds(getDoc(doc(memberDb, "products/p-1001/reviews/r-public")));await assertFails(getDoc(doc(memberDb, "products/p-1001/reviews/r-private")));

Production judgment

Path matching should make the authorization model legible. Prefer explicit matches for important resource families, use recursive wildcards for intentional collection-group patterns rather than convenience, and treat each cross-document lookup as a dependency with correctness, billing and availability implications. A path model that requires complex rules “joins” is often signaling that authorization-relevant fields should be remodeled.

Lesson 3 applies that matcher precisely to operation types and field-level create/update validation.

Knowledge check

  1. Do rules on a parent document automatically apply to its subcollections?
  2. What changes for recursive wildcards in Rules v2?
  3. What happens if two matching allow rules disagree?
  4. What are the main document-access call limits?
  5. Why can get()/exists() affect cost?
Review the answers

1. No. Subcollection documents need rules matching their own paths.

2. They match zero or more path segments; v2 is also required for collection-group query rules.

3. The request is allowed if any matching allow expression is true.

4. 10 for single-document/query requests; 20 for multi-document reads, transactions and batches, while the per-operation 10-call limit still applies.

5. They execute document reads for rule evaluation in production, including on some rejected requests.

Summary

Rules are path programs with bounded cross-document context. AtlasMart now has predictable wildcard semantics, explicit descendant rules and an authorization design that respects access-call limits.

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.