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.
Learning outcomes
Predict which document paths a nested match, single-segment wildcard or recursive wildcard covers under Rules v2.
Use get(), exists() and getAfter() only when cross-document policy is worth the read, complexity and evaluation limits.
Explain the 10-call and 20-call document-access limits and how per-operation limits still constrain batches/transactions.
Detect dangerous overlap where a broad matching allow silently grants access that a narrower rule appears to deny.
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.
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.
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
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.
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.
rules_version = '2';service cloud.firestore { match /databases/{database}/documents { match /{path=**}/reviews/{reviewId} { allow get, list: if resource.data.visibility == "public"; } }}
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.
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.
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
{ "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" }}
{ "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, "emulators": { "firestore": { "port": 8080 }, "auth": { "port": 9099 }, "ui": { "enabled": true, "port": 4000 } }}
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
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.
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
- Do rules on a parent document automatically apply to its subcollections?
- What changes for recursive wildcards in Rules v2?
- What happens if two matching allow rules disagree?
- What are the main document-access call limits?
- 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
- Get started with Cloud Firestore Security Rules
- Structuring Cloud Firestore Security Rules
- Writing conditions for Cloud Firestore Security Rules
- Securely query data
- Control access to specific fields
- Test your Cloud Firestore Security Rules
- Build unit tests for Firebase Security Rules
- @firebase/rules-unit-testing reference
- Secure data access for users and groups
- Firestore SDKs and client libraries
- Enterprise security overview
- Security Rules for Pipeline operations
- Enterprise Native Core and Pipeline operations overview
- Firebase App Check
- Firestore IAM