Firestore Security Rules: Enforcing Per-User Access Control with Owner Fields
Learn to enforce per-user data isolation in Firestore using security rules that match request.auth.uid to an owner field. Includes a minimal rule, create vs. update handling, emulator testing steps, and common mistakes that silently break access control.
25 Jul 2025, 08:52 UTC

The Problem: Users Must Only See Their Own Data
In a multi-user Firestore application, the most common security requirement is ensuring that a user can read and write only documents they own. Client-side checks are insufficient because a compromised client or a malicious API call can bypass them. Firestore security rules run on the server before any operation reaches the database, making them the correct enforcement point.
Useful Takeaway
Write a rule that compares request.auth.uid to an owner field stored on each document. Keep the expression simple: allow read, write: if request.auth != null && request.auth.uid == resource.data.owner;. This single line, placed on the appropriate collection path, provides per-user isolation for reads and writes.
Minimal Working Rule
Assume a posts collection where every document contains an owner string field set to the creator's UID. The following rule set, deployed to the cloud.firestore service, enforces ownership for all operations on that collection.
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.owner;
}
}
}Key points:
request.authis populated by Firebase Authentication when the client includes a valid ID token.resource.datarepresents the existing document data duringread,update, anddeleteoperations.- For
createoperations,resource.dataisnullbecause the document does not yet exist. Userequest.resource.data.ownerinstead (see thecreate-specific rule below).
Handling Document Creation
When a user creates a new post, the rule must verify that the owner field in the incoming data matches the caller's UID. Split the write permission into create and update, delete to reference the correct data object.
match /posts/{postId} {
allow read: if request.auth != null
&& request.auth.uid == resource.data.owner;
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.owner;
allow update, delete: if request.auth != null
&& request.auth.uid == resource.data.owner;
}This pattern prevents a user from creating a document with someone else's UID in the owner field.
Testing Locally with the Emulator Suite
Before deploying, validate rules against realistic data using the Firebase Local Emulator Suite. This avoids accidental lockouts or data leaks in production.
- Install the Firebase CLI (
npm install -g firebase-tools) and log in withfirebase login(requires project Owner or Editor role). - Initialize a test project directory:
firebase init firestoreand select Use existing project or create a new one. - Place the rule file at
firestore.rulesin the project root. - Create a sample data file
data/posts.jsonwith documents that include anownerfield, e.g.:
{
"posts": {
"post1": { "title": "Hello", "owner": "uid-alice" },
"post2": { "title": "World", "owner": "uid-bob" }
}
}- Start the emulator with imported data:
firebase emulators:start --import=./data. The Firestore emulator will be available athttp://localhost:8080. - Run scripted tests using the Node.js Admin SDK or the REST API with different
Authorization: Bearer <uid>headers (the emulator accepts any string as a mock UID). Example test script:
// test-rules.js
const { initializeApp, applicationDefault } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
initializeApp({ credential: applicationDefault(), projectId: 'demo-project' });
const db = getFirestore();
async function test() {
// Simulate Alice reading her own post
const aliceDoc = db.collection('posts').doc('post1');
// The emulator respects the auth token set via FIRESTORE_EMULATOR_HOST
// and the 'Authorization' header if using REST.
// For Admin SDK, rules are bypassed; use REST for rule testing.
}
test();For quick manual checks, use curl against the emulator REST endpoint:
curl -H "Authorization: Bearer uid-alice" \
http://localhost:8080/v1/projects/demo-project/databases/(default)/documents/posts/post1Expect a 200 response for the owner, 403 for a different UID. Check the emulator console for rule evaluation traces showing the owner comparison.
Rule Limits and Performance Considerations
- Evaluation cost: Firestore evaluates rules on every read and write. Complex boolean logic, multiple
get()orexists()calls, or custom functions increase latency and count against the per-request evaluation budget (default 10 function calls per request). Keep expressions flat. - Custom functions: You can define helper functions like
function isOwner(uid) { return request.auth.uid == uid; }, but each call adds evaluation overhead. Inline the comparison for the simplest rules. - No size enforcement: Rules cannot limit the byte size of a document or field. If you need to cap payload size, validate in a Cloud Function before write or use client-side guards (with the understanding they are not a security boundary).
- Instant propagation: Rule changes deploy globally within seconds. No cache invalidation is required, but a bad rule can immediately block legitimate traffic. Always test in the emulator first.
Common Pitfalls
| Mistake | Symptom | Fix |
|---|---|---|
Using resource.data.owner in a create rule | Rule evaluation error (null reference) or unintended allow | Use request.resource.data.owner for create |
Typing request.auth.id instead of request.auth.uid | Silently allows all access because request.auth.id is undefined, making the comparison undefined == "some-uid" false, but if combined with || it can pass | Always use request.auth.uid; lint rules with firebase firestore:rules:lint |
| Relying on client-side ownership checks only | Malicious user modifies UID in request and accesses others' data | Enforce ownership in rules; client checks are UX only |
Referencing resource.data on a document that may not exist | Rule evaluation error on read of missing doc | Guard with resource != null or use exists() before accessing fields |
Overly broad match /{document=**} with per-user logic | Rules become unmaintainable and expensive | Scope rules to specific collections; use sub-collection matches for hierarchical data |
Practical Verification Checklist
- Deploy the rule to a test project (not production) via
firebase deploy --only firestore:rules. - In the Firebase console, open the Firestore Rules tab and use the Rules Playground to simulate reads/writes with different
auth.uidvalues. - Run the emulator test suite (step 6 above) and confirm 403 responses for cross-user access.
- Inspect emulator logs for
PERMISSION_DENIEDentries that show the evaluated expression. - After confirming behavior, promote the rule to staging/production with a staged rollout if possible.
Limitations to Keep in Mind
Security rules are not a replacement for application-level validation. They cannot:
- Enforce referential integrity across collections (e.g., a post's
authormust exist inusers). - Perform complex transactions or multi-document atomic checks.
- Rate-limit abusive clients (use Firebase App Check or Cloud Functions for that).
For those needs, combine rules with Cloud Functions that run with administrative privileges and perform additional checks before committing writes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.