Enforcing Per‑User Data Ownership in Firestore with Security Rules
Learn how to enforce per‑user data ownership in Firestore by storing an owner UID on each document and writing security rules that compare it against the caller’s UID. Includes a concrete chat‑room example, trade‑offs, and a verification checklist.
14 Sept 2025, 14:41 UTC

Why Firestore Needs Ownership Rules
In a shared‑database app, every client can issue reads and writes to any document in a collection. If you leave the default rules (allow read, write: if true;) in place, a malicious or misconfigured user can read or delete any user’s data. The solution is to encode ownership directly into the document and write a rule that checks that the caller’s UID matches the stored owner ID.
Rule Fundamentals
request.auth.uid– the UID of the authenticated user making the request.resource.data– the existing document data (for reads and updates).request.resource.data– the data the client is trying to write.
All expressions are evaluated on the server; they run before any data is committed.
Storing Ownership
Add an ownerUid field to every document that should be protected. For example, a chat message might look like:
{
"text": "Hello world",
"ownerUid": "user_abc123",
"timestamp": 1678881234
}
When a user creates a message, you should set ownerUid to request.auth.uid on the client or via a Cloud Function.
Rule Syntax for Ownership
Below is a minimal rule set that:
- Allows a user to create a document only if the UID matches.
- Allows a user to read or update a document only if they own it.
- Denies all other access.
service cloud.firestore {
match /databases/{database}/documents {
match /messages/{messageId} {
// Creation
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.ownerUid;
// Read, update, delete
allow read, update, delete: if request.auth != null &&
request.auth.uid == resource.data.ownerUid;
}
}
}
Replace /messages with the path of your collection. For nested collections, use a wildcard or include the parent document’s ownerUid in the rule.
Worked Example: A Simple Chat Room
Assume a chat room collection rooms/{roomId}/messages/{messageId}. Each room has a participants array listing the UIDs that can read the room. The rule for messages is:
service cloud.firestore {
match /databases/{database}/documents {
match /rooms/{roomId} {
function isParticipant() {
return request.auth.uid in resource.data.participants;
}
match /messages/{messageId} {
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.ownerUid &&
isParticipant();
allow read, update, delete: if request.auth != null &&
request.auth.uid == resource.data.ownerUid &&
isParticipant();
}
}
}
}
Here, isParticipant() is a helper that checks membership in the parent room’s array. The rule ensures only participants can create or read messages, and only the message owner can modify or delete it.
Trade‑offs and Limitations
- Rule Complexity vs. Performance: Nested functions and array checks are fine for small arrays, but iterating over large participant lists can increase latency and cost. Keep arrays short or use a sub‑collection of participant documents.
- No Input Sanitization: Security rules only enforce access; they do not validate data types or formats. You must still validate on the client or via Cloud Functions.
- Debugging Difficulty: A mis‑typed field name or wrong path can silently deny legitimate requests. Always test locally before deploying.
Verification Checklist
- Set up the Firebase Emulator Suite:
firebase init emulators # choose Firestore and auth firebase emulators:start --only firestore,auth - Write the
firestore.rulesfile with your ownership rules. - Use the Rules Unit Testing library to write tests:
const { initializeTestApp, assertFails, assertSucceeds } = require('@firebase/rules-unit-testing'); const app = initializeTestApp({ projectId: 'demo', auth: { uid: 'user1' } }); const db = app.firestore(); // test creating a message await assertSucceeds(db.collection('rooms/room1/messages').add({ text: 'Hi', ownerUid: 'user1', timestamp: Date.now() })); await assertFails(db.collection('rooms/room1/messages').add({ text: 'Oops', ownerUid: 'user2', timestamp: Date.now() })); - Deploy the rules with
firebase deploy --only firestore:rules. - In the Firebase Console, open the Rules Playground, paste a sample request, and confirm the expected allow/deny.
Actionable Closing
1. Add ownerUid to every sensitive document. 2. Write a concise rule that compares request.auth.uid to that field. 3. Test thoroughly with the Emulator Suite. 4. Deploy and monitor the Rules Playground for unexpected denials. 5. Remember to validate data formats on the client or in Cloud Functions to complement the rules.
By following these steps you can protect user data in a multi‑user Firestore app without sacrificing usability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.