How to Write Firestore Rules
Introduction Firestore is one of the most powerful NoSQL databases offered by Google Firebase, enabling real-time data synchronization across web and mobile applications. Its flexibility and scalability make it a favorite among developers building modern applications. However, with great power comes great responsibility — and nowhere is this more critical than in the security rules that govern dat
Introduction
Firestore is one of the most powerful NoSQL databases offered by Google Firebase, enabling real-time data synchronization across web and mobile applications. Its flexibility and scalability make it a favorite among developers building modern applications. However, with great power comes great responsibility and nowhere is this more critical than in the security rules that govern data access.
Firestore security rules determine who can read or write to your database, under what conditions, and with what constraints. A single misconfigured rule can expose sensitive user data, allow malicious writes, or open your application to abuse. Many developers assume that because Firestore is easy to set up, its also easy to secure but that assumption is dangerously wrong.
This article presents the top 10 proven strategies to write Firestore rules you can trust. These arent just best practices theyre battle-tested principles used by teams managing millions of users and petabytes of data. Whether youre building a social app, a SaaS platform, or a real-time collaboration tool, these rules will help you build a secure, scalable, and reliable data layer.
By the end of this guide, youll understand how to avoid the most common security pitfalls, enforce data integrity, and structure rules that are both maintainable and auditable. Trust in your database isnt optional its foundational. Lets build it right.
Why Trust Matters
Trust in your Firestore database isnt a luxury its a necessity. Unlike traditional server-side databases where backend logic controls access, Firestore relies entirely on client-side rules. This means every request whether from a mobile app, a browser, or a third-party integration is evaluated against these rules before any data is read or written. Theres no middleware, no server-side validation layer to fall back on. If the rule is flawed, the data is compromised.
Consider the consequences of insecure rules:
- A user could read all private messages in a chat app because a rule mistakenly allowed request.auth != null without checking document ownership.
- An attacker could overwrite every user profile by exploiting a rule that permits writes to /users/{userId} without validating the incoming data structure.
- Malicious actors might flood your database with fake entries, exhausting your quota and increasing your billing costs.
These arent hypothetical scenarios. Real-world breaches have occurred because developers assumed it works locally, so its fine in production. Firestores emulator is excellent for testing functionality, but it doesnt replicate the full security context of a live environment. Rules that pass in the emulator may still fail under real-world attack vectors.
Trust also impacts user retention and brand reputation. If users learn that their data was exposed due to poor security, they wont return regardless of how elegant your UI or how fast your app performs. Security isnt a checkbox; its a continuous commitment.
Moreover, compliance frameworks like GDPR, HIPAA, and CCPA require strict data access controls. Insecure Firestore rules can put your organization at legal risk. Even if your app doesnt handle sensitive health or financial data, user identifiers, location history, and behavioral patterns are still personal data under most regulations.
Writing trustworthy Firestore rules requires a mindset shift: treat every rule as if its being actively exploited. Assume bad actors are probing your endpoints. Assume they know your collection names. Assume theyre scripting automated attacks. Your rules must be defensive, explicit, and layered not optimistic or permissive.
In this context, trust means more than just preventing unauthorized access. It means ensuring data integrity, enforcing business logic, maintaining auditability, and enabling scalability without sacrificing security. The top 10 strategies outlined below are designed to help you achieve exactly that.
Top 10 How to Write Firestore Rules You Can Trust
1. Always Use Authenticated Users for Sensitive Operations
Never allow unauthenticated users to read or write to collections that contain personal, private, or sensitive data. Even if your app allows guest access, isolate guest data into a separate, clearly defined collection with minimal permissions.
For example, if your app has a /messages collection where users exchange private content, your rule must verify authentication:
match /databases/{database}/documents/messages/{messageId} {
allow read, write: if request.auth != null;
}
But this is only the first step. Simply checking request.auth != null is insufficient if youre not verifying ownership. A user should only read messages they sent or received. Use request.auth.uid to tie data to the authenticated user:
match /databases/{database}/documents/messages/{messageId} {
allow read, write: if request.auth != null &&
(resource.data.senderId == request.auth.uid ||
resource.data.receiverId == request.auth.uid);
}
Always assume that any data accessible to authenticated users must be scoped to their identity. Never rely on client-side input to determine ownership validate it server-side via rules. This prevents users from tampering with senderId or receiverId fields in their requests.
Additionally, avoid using request.auth.token.email or request.auth.token.name for authorization logic. These values can be changed by the user in their Firebase Authentication profile and are not reliable for access control. Use request.auth.uid the unique, immutable identifier as your primary key for user-based permissions.
2. Validate Data Structure with request.resource
One of the most common security flaws in Firestore rules is allowing writes without validating the structure of the incoming data. Attackers can inject malformed or malicious fields, overwrite critical fields, or add unexpected properties that break your application logic.
Use request.resource.data to inspect the data being written. Combine it with exists() and newData to ensure only expected fields are modified. For example, in a /users collection where each document should have displayName, email, and createdAt:
match /databases/{database}/documents/users/{userId} {
allow create: if request.auth != null &&
request.resource.data.keys().hasOnly(['displayName', 'email', 'createdAt']) &&
request.resource.data.displayName is string &&
request.resource.data.email is string &&
request.resource.data.createdAt is number;
allow update: if request.auth != null &&
request.auth.uid == userId &&
request.resource.data.keys().hasOnly(['displayName']) &&
request.resource.data.displayName is string;
}
This rule ensures:
- Only allowed fields can be set on creation.
- On update, only displayName can be changed email and createdAt are immutable.
- Each field has the correct data type.
Never allow wildcard updates like allow update: if request.auth != null; this opens the door to data corruption. Even if your app doesnt currently use certain fields, attackers may exploit them to inject scripts, manipulate rankings, or trigger backend errors.
For nested objects, validate each level explicitly:
request.resource.data.profile.avatarUrl is string &&
request.resource.data.profile.bio is string &&
request.resource.data.profile.socialLinks is map &&
request.resource.data.profile.socialLinks.keys().hasOnly(['twitter', 'linkedin'])
Validation isnt optional its your first line of defense against data poisoning.
3. Enforce Ownership with Document ID Matching
Many developers write rules that allow users to access any document in a collection, assuming that request.auth.uid will be checked elsewhere. This is a dangerous assumption. Always bind document access to the authenticated users ID preferably by matching the document ID to request.auth.uid.
For example, if each user has their own profile document, structure your collection like this:
- /users/{userId} where {userId} is the Firebase Auth UID.
Then write your rule as:
match /databases/{database}/documents/users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
This rule is elegant and secure. It ensures that only the user with UID matching the document ID can access that document. No additional checks are needed. Even if a malicious user tries to access /users/abc123 while authenticated as xyz789, the rule will deny access because request.auth.uid != userId.
This pattern scales cleanly. You can extend it to subcollections:
match /databases/{database}/documents/users/{userId}/posts/{postId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null && request.auth.uid == userId;
}
Never use dynamic document IDs like /posts/{postId} and then rely on a field inside the document to determine ownership. Attackers can guess or enumerate document IDs. Always make the UID part of the path.
For shared resources like a /teams collection use a separate mechanism (e.g., a members map field) to track access, but never rely on document ID alone. Ownership by path is the most reliable pattern.
4. Avoid Wildcard Rules in Root Collections
Its tempting to write a rule like:
match /databases/{database}/documents/{document=**} {
allow read, write: if request.auth != null;
}
This grants authenticated users access to every document in your entire database. Its easy to write, but catastrophic in production. Even if you think youre only using it for development, you may accidentally deploy it to production and once its live, its nearly impossible to revert without data loss.
Wildcards at the root level bypass all structure and intent. They ignore your data model. They treat /payments, /admin, and /private-messages as if theyre the same. This violates the principle of least privilege and makes audits impossible.
Instead, define explicit paths for every collection and subcollection:
match /databases/{database}/documents/users/{userId} { ... }
match /databases/{database}/documents/products/{productId} { ... }
match /databases/{database}/documents/orders/{orderId} { ... }
match /databases/{database}/documents/admin/settings { ... }
If you have many similar collections, use a shared function to reduce repetition but never a wildcard.
Even if youre building a prototype, write rules as if theyll go live tomorrow. The cost of fixing a misconfigured wildcard later is far higher than the time spent writing explicit rules now.
As a rule of thumb: if you find yourself writing {document=**}, stop. Ask yourself: Is there a way to structure this without a root wildcard? The answer is almost always yes.
5. Use Functions to Reuse and Abstract Logic
As your Firestore rules grow in complexity, duplicating logic across multiple rules becomes unmanageable and error-prone. Use function declarations to abstract common conditions and ensure consistency.
For example, if multiple collections require that only the document owner can write to them, create a reusable function:
function isOwner(userId) {
return request.auth != null && request.auth.uid == userId;
}
match /databases/{database}/documents/users/{userId} {
allow read, write: if isOwner(userId);
}
match /databases/{database}/documents/profiles/{userId} {
allow read, write: if isOwner(userId);
}
match /databases/{database}/documents/notes/{noteId} {
allow read, write: if isOwner(resource.data.userId);
}
Functions also help with complex validations:
function isValidUserUpdate() {
return request.resource.data.keys().hasOnly(['displayName', 'bio']) &&
request.resource.data.displayName is string &&
request.resource.data.bio is string &&
request.resource.data.displayName.size() > 0;
}
match /databases/{database}/documents/users/{userId} {
allow update: if isOwner(userId) && isValidUserUpdate();
}
Functions improve readability, reduce bugs, and make audits easier. When reviewing rules, an auditor can quickly understand isOwner() without parsing the same logic in five places.
Remember: functions are evaluated at rule execution time. They dont store state or call external APIs. Theyre pure functions deterministic and fast. Use them liberally to keep your rules clean and maintainable.
Also, avoid nesting functions too deeply. Keep them simple and focused. One function = one responsibility.
6. Test Rules in the Firebase Emulator Suite
Never deploy Firestore rules without testing them in the Firebase Emulator Suite. The emulator allows you to simulate real authentication states, data writes, and access patterns all without affecting production data.
Set up a test suite using Jest, Mocha, or even simple Node.js scripts that call the emulators REST API. For example:
// Test: User A should not read User Bs profile
const response = await fetch('http://localhost:8080/firestore/v1/projects/my-app/databases/(default)/documents/users/B', {
headers: { 'Authorization': 'Bearer userA-token' }
});
assert.equal(response.status, 403);
Write test cases for:
- Authenticated vs. unauthenticated access
- Ownership validation
- Invalid data types
- Field-level restrictions
- Subcollection access
- Batch writes and transactions
Use the Firebase Emulator UI to manually explore your rules and simulate edge cases. Try writing a document with a missing field. Try updating a document with a malformed timestamp. Try accessing a document with a guessed ID.
Automate these tests in your CI/CD pipeline. If rules fail a test, block deployment. Treat your rules like production code they deserve unit tests, code reviews, and version control.
Remember: the emulator doesnt catch everything. It doesnt simulate rate limiting, billing quotas, or network latency. But it catches 95% of logic errors. Thats enough to prevent catastrophic failures.
7. Limit Data Exposure with Field-Level Access Control
Dont grant read access to entire documents if only a subset of fields is needed. Firestore rules support field-level access using resource.data.fieldName and request.resource.data.fieldName in conditional logic.
For example, in a /users collection, you might want public profiles to show only displayName and avatarUrl, while private fields like email, phoneNumber, and lastLogin are only visible to the user or admins:
match /databases/{database}/documents/users/{userId} {
// Public read: anyone can see basic profile
allow read: if request.auth != null || resource.data.visibility == 'public';
// Private fields only readable by owner or admin
match /users/{userId} {
allow read: if isOwner(userId) || isAdmin();
}
}
But wait this doesnt work. Firestore rules dont allow partial reads. If a user has read access to a document, they get the entire document. To enforce field-level privacy, you must split data into separate collections.
Instead, structure your data like this:
- /publicProfiles/{userId} contains displayName, avatarUrl, bio
- /privateProfiles/{userId} contains email, phoneNumber, lastLogin, preferences
Then apply different rules to each:
match /databases/{database}/documents/publicProfiles/{userId} {
allow read: if true; // Publicly readable
allow write: if isOwner(userId);
}
match /databases/{database}/documents/privateProfiles/{userId} {
allow read, write: if isOwner(userId) || isAdmin();
}
This approach ensures that sensitive data is never exposed, even if a rule is misconfigured on the public collection. It also improves performance clients only fetch the data they need.
Field-level access control isnt about restricting what users can see its about designing your data model to minimize exposure. Always ask: Whats the minimum data needed for this operation? Then structure your collections accordingly.
8. Prevent Enumeration Attacks with Time-Based and Rate-Limited Rules
Attackers often try to enumerate data by guessing document IDs especially in collections like /users, /orders, or /products. If your rules allow read access to any document with a valid UID, they can brute-force IDs to discover user accounts or order histories.
To prevent this, limit read access to only authenticated users who have a legitimate reason to access the data and never allow listing entire collections without filters.
For example, avoid this rule:
match /databases/{database}/documents/users/{userId} {
allow read: if request.auth != null; // Allows listing all users!
}
This lets any authenticated user list all user documents by querying /users. Instead, enforce access only on known documents:
match /databases/{database}/documents/users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
}
Now, a user can only read their own profile. To find other users, they must be explicitly invited or granted access via a shared collection (e.g., /friendships).
For collections that require listing like /posts use queries with filters and enforce them in rules:
match /databases/{database}/documents/posts {
allow read: if request.auth != null &&
request.query.where == 'status' &&
request.query.value == 'published';
}
This ensures users can only read posts with status = published and cannot query for status = private or userId = *.
Additionally, consider adding rate-limiting logic using request.time and resource.data.lastAccessed to detect and block repeated failed attempts. While Firestore doesnt have built-in rate limiting, you can simulate it:
function isNotRateLimited() {
let lastAttempt = resource.data.lastAttemptTime;
if (lastAttempt == null) return true;
return request.time.seconds - lastAttempt > 60; // 1 minute cooldown
}
Update lastAttemptTime on every write attempt. This wont stop determined attackers, but it will slow them down and generate logs for detection.
9. Use Security Rules with Firestore Queries
Firestore security rules and queries are tightly coupled. A rule that allows reading a document does not automatically allow reading a collection and vice versa. Queries must satisfy the same constraints as individual document reads.
For example, if your rule is:
match /databases/{database}/documents/messages/{userId} {
allow read: if request.auth.uid == userId;
}
Then this query will fail:
db.collection('messages').where('senderId', '==', 'abc123').get()
Why? Because the rule requires the document ID to match request.auth.uid, but the query is filtering by senderId. Firestore evaluates rules based on the path and the query not the filter.
To fix this, restructure your data:
- Store messages under /users/{userId}/messages/{messageId}
Then use this rule:
match /databases/{database}/documents/users/{userId}/messages/{messageId} {
allow read: if request.auth.uid == userId;
}
Now the query becomes:
db.collection(users/${currentUser.uid}/messages).get()
This satisfies the rule because the path matches userId, and the user is authenticated.
Always design your data model with queries in mind. If you need to query by a field, make sure your rules can enforce that fields access using the document path or a known structure.
Also, avoid using orderBy or limit without validating them in rules. Attackers can abuse these to drain bandwidth or trigger expensive operations:
match /databases/{database}/documents/posts {
allow read: if request.auth != null &&
request.query.limit
request.query.orderBy == 'createdAt';
}
These constraints prevent clients from requesting 10,000 posts at once or ordering by arbitrary fields.
10. Review, Version, and Monitor Rules Regularly
Firestore rules are code. Treat them as such. Store them in version control (Git), review them in pull requests, and deploy them through CI/CD pipelines. Never edit rules directly in the Firebase Console its a recipe for untracked changes and accidental overwrites.
Use the Firebase CLI to deploy rules:
firebase deploy --only firestore:rules
Keep your rules in a file like firestore.rules and use comments to document intent:
// Only admins can delete users
// Prevents accidental or malicious account removal
match /databases/{database}/documents/users/{userId} {
allow delete: if isAdmin();
}
Set up automated monitoring. Use Firebase Logging and Cloud Monitoring to track rule violations. Look for spikes in DENY events they may indicate brute-force attacks or misbehaving clients.
Perform quarterly audits of your rules. Ask:
- Are there any rules that allow read: if true?
- Are there any wildcards?
- Are all writes validated?
- Are ownership checks consistent?
Invite a second developer to review your rules. Fresh eyes catch what you miss.
Finally, subscribe to Firebase release notes. New rule features like request.auth.token.email_verified or resource.data enhancements may improve your security posture. Stay updated. Stay secure.
Comparison Table
The table below compares common rule patterns both insecure and secure to highlight the difference between risky assumptions and trustworthy practices.
| Scenario | Insecure Rule | Trustworthy Rule | Why Its Better |
|---|---|---|---|
| User profile access | match /users/{userId} { allow read, write: if request.auth != null; } | match /users/{userId} { allow read, write: if request.auth.uid == userId; } | Prevents users from accessing others profiles by enforcing ownership via document ID. |
| Data validation | match /posts/{postId} { allow write: if request.auth != null; } | match /posts/{postId} { allow write: if request.auth != null && request.resource.data.title is string && request.resource.data.content is string && request.resource.data.keys().hasOnly(['title', 'content', 'createdAt']); } | Prevents injection of malicious or malformed fields and ensures data integrity. |
| Collection listing | match /databases/{database}/documents/{document=**} { allow read: if request.auth != null; } | match /databases/{database}/documents/posts/{postId} { allow read: if request.auth != null && resource.data.visibility == 'public'; } | Eliminates root wildcards and restricts access to specific collections with defined conditions. |
| Query filtering | Query: .where('userId', '==', 'abc123') Rule: allow read: if true; |
Query: .where('status', '==', 'published') Rule: allow read: if request.query.where == 'status' && request.query.value == 'published'; |
Ensures queries are constrained and cannot bypass security by using arbitrary filters. |
| Field-level privacy | Single collection /users with email, phoneNumber, displayName all readable by authenticated users. | Two collections: /publicProfiles (readable by all) and /privateProfiles (readable only by owner). | Minimizes exposure of sensitive data even if a rule is accidentally relaxed. |
Use this table as a reference during code reviews. When you see an insecure pattern, replace it with the trustworthy alternative immediately.
FAQs
Can Firestore rules prevent data deletion?
Yes. Use the delete operation in your rules to control who can remove documents. For example: allow delete: if request.auth.uid == userId && resource.data.status != 'locked';. You can also prevent deletion entirely by omitting the delete rule if a rule doesnt allow deletion, its denied by default.
Do Firestore rules protect against batch writes?
Yes. Firestore rules apply to every operation in a batch write or transaction. Each document access is evaluated individually. If one operation violates a rule, the entire batch fails. This ensures atomicity and security.
Can I use Firestore rules to validate email domains?
Yes. You can check request.auth.token.email and validate its domain: request.auth.token.email.matches('.*@company.com'). However, this only works if users sign up via email authentication. For third-party providers (Google, Apple), use request.auth.token.email if available, or rely on custom claims for domain verification.
How do I handle admin roles in Firestore rules?
Use Firebase Custom Claims. Set a claim like admin: true via the Admin SDK, then check it in rules: allow write: if request.auth.token.admin == true;. Never rely on client-side data to determine admin status.
What happens if I change my Firestore rules while users are connected?
Changes to rules are applied instantly. Clients receive a permission error on their next request. They must re-authenticate or refresh their connection to apply the new rules. No data is lost only access is restricted or granted based on the updated rules.
Can I use Firestore rules to limit the number of documents a user can create?
Yes. Use get() to count existing documents in a collection and enforce limits. Example: allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)/posts).data.count
Are Firestore rules encrypted?
Rules themselves are not encrypted they are stored as plain text in the Firebase project. However, they are only evaluated server-side and never sent to clients. As long as you store them in version control securely and restrict access to your Firebase project, they remain protected.
Do Firestore rules work with Firebase App Check?
Yes. You can combine rules with App Check to ensure requests come from your authentic app. Use request.auth != null && request.appCheck != null to require both authentication and App Check verification. This adds an extra layer against API abuse.
How do I test rules with multiple users?
Use the Firebase Emulator Suite with the auth option to simulate different users. In your test scripts, generate tokens for userA, userB, etc., and send requests with each token. The emulator supports multiple authenticated sessions simultaneously.
Whats the maximum size for Firestore rules?
The maximum size for a single rules file is 64 KB. If your rules exceed this, refactor them into functions, simplify conditions, or split logic across multiple files (if using the Firebase CLI with multiple rule files).
Conclusion
Writing trustworthy Firestore rules is not about finding the perfect syntax its about adopting a security-first mindset. Every rule you write is a contract between your application and its users. Break that contract once, and trust is lost forever.
The top 10 strategies outlined in this guide are not suggestions they are requirements for any application that values data integrity, user privacy, and long-term reliability. From enforcing ownership through document paths to validating every field and testing relentlessly, each principle reinforces a single truth: security is not an afterthought.
Remember: the most elegant code is useless if it leaks data. The fastest app is irrelevant if users cant trust it. Your Firestore database holds the heartbeat of your application. Protect it like your most valuable asset because it is.
Start today. Audit your existing rules. Replace every wildcard. Validate every write. Test every path. Use functions. Structure for queries. Monitor for anomalies. And never, ever assume that it works on my machine is enough.
Trust isnt given. Its built one secure rule at a time.