Enforce Document Ownership in Firestore with Security Rules
Use Firestore security rules to compare request.auth.uid to resource.data.ownerId, then test locally before deploying.
18 Sept 2025, 03:11 UTC

The problem and the useful answer
If your Firestore database stores user-specific data, hiding a button or filtering query results in the application code is not sufficient protection. A malicious user can initialize the Firestore SDK directly and read or write any document the security rules allow. The useful takeaway is to enforce ownership on the server: store an `ownerId` field on each document, then write a rule that compares `request.auth.uid` to `resource.data.ownerId` before permitting a read or write. Test the rule locally with the Firebase Emulator Suite before deploying it to production.
How Firestore security rules are evaluated
Firestore security rules are declarative expressions evaluated by Firebase on the server for every document read and write request. The rules language runs independently of your application code, so a compromised or modified client cannot bypass them. When a request arrives, Firestore checks the rules that match the target document path. If no matching rule returns `true`, the request is denied with a permission-denied error.
Two objects are central to ownership rules:
- `request.auth` — contains the authenticated user's ID and token. If the request is unauthenticated, `request.auth` is `null`.
- `resource.data` — contains the current field values of the document being read or written. For a write request, this is the document state before the operation.
Worked configuration: owner-only access to a tasks collection
Assume a `tasks` collection where each document has an `ownerId` field set to the creating user's UID. The following rules file restricts reads and writes to the document owner.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{taskId} {
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.ownerId;
}
}
}
`rules_version = '2';` enables the newer rules language behavior, including recursive wildcards if you later add subcollections. `match /tasks/{taskId}` binds the document ID to `taskId`, though this example does not use the variable. The expression `request.auth.uid == resource.data.ownerId` evaluates to `true` only when the authenticated user matches the stored owner.
Deploying the rules file
Save the file as `firestore.rules` in your Firebase project directory. From a terminal in that directory, run:
firebase deploy --only firestore:rules
Where to run: your local project directory that contains `firebase.json` and `firestore.rules`. Required permissions: a Firebase CLI session authenticated with `firebase login`, and a project role that can modify Firestore rules, such as Owner or Editor. Expected check: the CLI reports that the rules were deployed successfully. Risk: a syntax error or overly broad rule can block legitimate users or expose data, so deploy first to a test project, not your production database.
Testing locally with the Emulator Suite
Start the local Firestore emulator before running rule tests:
firebase emulators:start --only firestore
Where to run: the same project directory. Required permissions: local file access and network access on the emulator port, typically 8080. Expected check: the emulator logs show that `firestore.rules` loaded without compilation errors. Risk: rules that pass in the emulator can still behave differently against production data, especially if they use `get()` to read other documents, so follow up with a production test project.
You can also write automated assertions with the `@firebase/rules-unit-testing` library. A test would initialize an authenticated client as UID A, attempt to write a task with `ownerId` set to UID B, and assert that the emulator rejects the request. A second test with matching UIDs should succeed.
Limits and common mistakes
- Broad rules expose data. `allow read, write: if true;` permits anyone on the internet to read and write your database. Never use this in production.
- Client-side filtering is not access control. Query filters such as `where("ownerId", "==", currentUserId)` only shape what the SDK requests; they do not stop a malicious client from requesting another user's document directly.
- Missing authentication check. If you omit `request.auth != null`, unauthenticated requests may pass when `resource.data.ownerId` is also null or missing.
- Unmatched documents are denied by default. If a document path matches no rule, Firestore returns permission-denied. This is safe by default, but it can lock out legitimate users after a collection rename or schema change.
- `get()` calls cost reads. Rules that call `get()` or `exists()` to inspect another document perform a Firestore read during evaluation. Frequent lookups increase latency and read costs.
- Recursive wildcards do not inherit permissions. `match /rooms/{roomId}/{document=**}` matches subcollections, but it does not automatically grant access based on a rule written for the parent `rooms` collection.
Practical verification after deployment
- Open your test project in the Firebase console and confirm the rules file matches what you deployed.
- From an unauthenticated web or mobile client, attempt to read a task document. Confirm the SDK returns a permission-denied error.
- From an authenticated client as a non-owner, attempt to update the same document. Confirm the request is rejected.
- From an authenticated client as the document owner, confirm the read and write succeed.
- Check Firestore read metrics in the Firebase console after deployment. An unexpected spike may indicate that rules are performing excessive `get()` calls on every request.
Rollback
Because deploying rules changes the production configuration, keep a copy of the previous `firestore.rules` file. To revert, restore the earlier file and run `firebase deploy --only firestore:rules` again. Verify the rollback by repeating the unauthenticated and non-owner access checks above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.