How to Write Firestore Rules
How to Write Firestore Rules Google Firestore is a powerful, scalable NoSQL document database built for modern mobile and web applications. Its real-time synchronization, offline support, and flexible data structure make it a top choice for developers. However, without proper security rules, even the most robust application can be vulnerable to data breaches, unauthorized access, or malicious writ
How to Write Firestore Rules
Google Firestore is a powerful, scalable NoSQL document database built for modern mobile and web applications. Its real-time synchronization, offline support, and flexible data structure make it a top choice for developers. However, without proper security rules, even the most robust application can be vulnerable to data breaches, unauthorized access, or malicious writes. Writing effective Firestore rules is not optional—it’s essential. These rules define who can read from or write to your database, under what conditions, and with what constraints. Mastering Firestore rules ensures your data remains secure, your users stay protected, and your application performs reliably at scale.
This comprehensive guide walks you through everything you need to know to write secure, efficient, and maintainable Firestore rules—from foundational syntax to advanced patterns and real-world examples. Whether you’re a beginner setting up your first project or an experienced developer refining production rules, this tutorial provides actionable insights backed by best practices and industry standards.
Step-by-Step Guide
Understanding Firestore Rule Structure
Firestore rules are written in a domain-specific language called the Firestore Security Rules Language. These rules are deployed as a single configuration file that applies to your entire database. The structure is hierarchical and declarative, meaning you define permissions based on conditions rather than procedural logic.
The basic syntax of a Firestore rule consists of three core components:
- Service: Declares the service you’re securing—in this case,
firestore. - Match: Defines the path to the documents or collections you’re protecting.
- Allow: Specifies the operations (read, write, create, update, delete) and the conditions under which they are permitted.
Here’s a minimal example:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null;
}
}
}
In this example, any authenticated user can read or write to their own user document under the /users/{userId} path. The {userId} is a wildcard that captures the document ID, making the rule reusable across all user documents.
Setting Up Your Firestore Project
Before writing rules, ensure your Firestore database is properly configured:
- Log in to the Firebase Console.
- Select your project or create a new one.
- Navigate to the Firestore Database section.
- Click on the Rules tab.
- By default, new projects start in test mode, where rules allow unrestricted access:
allow read, write: if true;. This is suitable only for development. - Switch to locked mode by replacing the default rules with your own secure configuration before deploying to production.
Always test your rules in the Firebase Emulator Suite before deploying. This allows you to simulate requests and validate behavior without affecting live data.
Writing Your First Secure Rule
Let’s build a simple rule set for a blog application with two collections: posts and comments.
Requirements:
- Anyone can read published posts.
- Only authenticated users can create or update posts.
- Only the post author can delete a post.
- Anyone can read comments.
- Only authenticated users can write comments.
- Users can only edit their own comments.
Here’s the rule set that fulfills these requirements:
service cloud.firestore {
match /databases/{database}/documents {
// Posts collection
match /posts/{postId} {
allow read: if true; // Public read access
allow write: if request.auth != null; // Authenticated users only
allow delete: if request.auth != null && request.auth.uid == resource.data.authorId;
}
// Comments collection
match /comments/{commentId} {
allow read: if true; // Public read access
allow write: if request.auth != null; // Only authenticated users
allow update: if request.auth != null && request.auth.uid == resource.data.userId;
allow delete: if request.auth != null && request.auth.uid == resource.data.userId;
}
}
}
Key elements explained:
request.auth != null: Ensures the user is authenticated.request.auth.uid: The unique identifier of the authenticated user.resource.data: Represents the data of the document before the write operation.resource: The document being accessed or modified.
Notice how we separate write from delete and update. This allows fine-grained control. For example, a user may be allowed to update a post but not delete it, depending on your business logic.
Using Wildcards and Path Variables
Wildcards like {userId} or {postId} capture dynamic segments of a document path. These are crucial for writing reusable, scalable rules.
Example: Restricting access to user-specific data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
This rule ensures that a user can only access their own document under the /users collection. The wildcard {userId} is referenced in the condition as userId (without braces).
Wildcards can also be nested:
match /users/{userId}/profile/{profileId} {
allow read: if request.auth.uid == userId;
}
This rule applies to documents nested under a user’s profile, such as /users/abc123/profile/bio. The condition ensures only the owner of the user document can read their profile data.
Validating Data with Request and Resource Objects
Firestore rules allow you to validate both incoming data (request.resource.data) and existing data (resource.data). This is critical for enforcing data integrity.
Example: Enforce that a post must have a title and content
match /posts/{postId} {
allow create: if request.auth != null &&
request.resource.data.title is string &&
request.resource.data.title != "" &&
request.resource.data.content is string &&
request.resource.data.content != "" &&
request.resource.data.publishedAt is timestamp;
}
Here, we validate:
- The
titleandcontentfields exist and are non-empty strings. - The
publishedAtfield is a valid timestamp.
For updates, compare incoming data with existing data to prevent unintended changes:
match /posts/{postId} {
allow update: if request.auth != null &&
request.auth.uid == resource.data.authorId &&
request.resource.data.title == resource.data.title && // Title cannot change
request.resource.data.content is string &&
request.resource.data.content.size() > 0;
}
This rule allows updating content but prevents modification of the title—a common requirement in content management systems.
Working with Custom Claims and Roles
For applications with role-based access control (RBAC), Firebase Authentication supports custom claims. These are key-value pairs attached to a user’s token and can be accessed in Firestore rules via request.auth.token.
Example: Assigning an admin role
On the server side (using Firebase Admin SDK), you might set a claim:
admin.auth().setCustomUserClaims(uid, { admin: true });
In Firestore rules:
match /admin/{documentId} {
allow read, write: if request.auth.token.admin == true;
}
Custom claims are cached in the user’s token, so changes may take up to an hour to reflect. For immediate updates, force token refresh on the client side after setting claims.
Testing Rules in the Emulator
Before deploying rules to production, test them using the Firebase Emulator Suite:
- Install the Firebase CLI:
npm install -g firebase-tools - Initialize your project:
firebase init - Select Firestore and Emulator Suite.
- Start the emulators:
firebase emulators:start - Use the Emulator UI at
http://localhost:4000to simulate reads and writes.
Write unit tests using the Firebase Rules Testing Library:
const { assertSucceeds, assertFails } = require("@firebase/rules-unit-testing");
describe("Firestore Rules", () => {
const testUserId = "test-user-123";
beforeEach(async () => {
await loadFirestoreRules({
projectId: "test-project",
rules: fs.readFileSync("firestore.rules", "utf8")
});
});
it("allows authenticated users to read their own document", async () => {
const db = getFirestore(testUserId);
const doc = db.collection("users").doc(testUserId);
await assertSucceeds(doc.set({ name: "John" }));
});
it("denies unauthenticated users from reading", async () => {
const db = getFirestore(); // Unauthenticated
const doc = db.collection("users").doc("any-user");
await assertFails(doc.get());
});
});
Testing ensures your rules behave as expected under various scenarios and reduces the risk of security gaps.
Best Practices
Principle of Least Privilege
Always grant the minimum level of access required. Avoid using allow read, write: if true; in production. Even if your app is internal, assume data can be accessed by malicious actors. Restrict access by user identity, document ownership, or role—never by default.
Use Explicit Conditions
Never rely on implicit assumptions. For example, don’t assume that if a user can write to a document, they can delete it. Be explicit:
allow delete: if request.auth.uid == resource.data.ownerId;
Explicit conditions are easier to audit, debug, and maintain.
Organize Rules by Collection
Structure your rules in a logical hierarchy. Group related collections together and use nested match statements to avoid repetition:
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
match /posts/{postId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
match /settings/{settingId} {
allow read, write: if request.auth.uid == userId;
}
}
This structure improves readability and reduces redundancy.
Avoid Deep Nesting
While Firestore allows deeply nested documents, complex rule paths can become hard to maintain. Prefer flat structures with reference fields when possible. For example, instead of:
/users/{userId}/posts/{postId}/comments/{commentId}/replies/{replyId}
Consider:
/posts/{postId}/comments/{commentId}
/comments/{commentId}/replies/{replyId}
This simplifies rule logic and improves performance.
Validate All Input
Always validate the shape, type, and content of incoming data. Malicious clients can send malformed payloads. Use is string, is number, is boolean, is timestamp, and size() to enforce constraints:
request.resource.data.email is string &&
request.resource.data.email.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
Regular expressions are supported for validating formats like emails, phone numbers, or slugs.
Use Functions for Reusability
Firestore rules support custom functions to avoid repetition. Define reusable logic at the top of your rules file:
function isOwner() {
return request.auth != null && request.auth.uid == resource.data.userId;
}
function isEmailVerified() {
return request.auth.token.email_verified == true;
}
match /users/{userId} {
allow read, write: if isOwner() && isEmailVerified();
}
Functions improve maintainability and make rules more readable.
Monitor and Log Access Patterns
Use Firebase Analytics and Cloud Logging to monitor unusual access patterns. Set up alerts for high volumes of denied requests, which may indicate brute-force attacks or misconfigured clients.
Never Trust Client-Side Data
Firestore rules are the final gatekeeper. Even if your app validates data on the frontend, malicious users can bypass client-side checks. All validation must occur in the rules layer.
Keep Rules Versioned
Treat your rules like code. Store them in your version control system (e.g., Git). Use meaningful commit messages:
- “Add comment deletion restriction”
- “Fix: Allow admin to update any post”
Deploy rules alongside your application code to ensure consistency between backend logic and security policies.
Review Rules Regularly
As your app evolves, so should your rules. Schedule quarterly reviews to:
- Remove unused rules
- Update permissions for new features
- Remove overly permissive conditions
- Verify custom claims are still relevant
Tools and Resources
Firebase Console Rules Editor
The Firebase Console provides a built-in editor for writing and testing rules. It includes syntax highlighting, real-time validation, and a simulation panel. While convenient, it lacks advanced testing capabilities—use the Emulator Suite for comprehensive validation.
Firebase Emulator Suite
The Firebase Emulator Suite is the most critical tool for rule development. It runs a local instance of Firestore, Authentication, and other services, allowing you to:
- Simulate authenticated and unauthenticated users
- Test edge cases without affecting live data
- Run automated unit tests
- Inspect data changes in real time
Install via npm: npm install -g firebase-tools
Firestore Rules Testing Library
This Node.js library enables automated testing of Firestore rules. It integrates with Jest and Mocha and allows you to write assertions like assertSucceeds() and assertFails().
GitHub: firebase/rules-unit-testing
Firestore Rules Linter (firestore-rules-lint)
A community-developed tool that checks your rules for common anti-patterns, such as overly broad permissions or missing validations.
Install: npm install -g firestore-rules-lint
Run: firestore-rules-lint firestore.rules
VS Code Extensions
Enhance your development experience with extensions:
- Firebase Snippets: Auto-complete common rule patterns.
- Firestore Rules Syntax Highlighting: Improves readability.
- FireRules: Provides linting and validation within the editor.
Documentation and Community
- Official Firestore Security Rules Guide
- Rules Language Reference
- Stack Overflow for troubleshooting
- Firebase Code Samples
Third-Party Tools
- Fireproof: A visual rule editor and simulator for complex rule sets.
- RuleGuard: Monitors your production rules for changes and alerts on potential vulnerabilities.
Real Examples
Example 1: Social Media App
Requirements:
- Users can follow other users.
- Posts are visible to followers and the author.
- Only the post author can delete their post.
- Comments are public but only the commenter can edit.
Rules:
service cloud.firestore {
match /databases/{database}/documents {
// Users collection
match /users/{userId} {
allow read: if true; // Public profile
allow write: if request.auth != null && request.auth.uid == userId;
}
// Follows collection (many-to-many relationship)
match /follows/{followerId}/{followingId} {
allow read: if request.auth != null && (request.auth.uid == followerId || request.auth.uid == followingId);
allow write: if request.auth != null && request.auth.uid == followerId;
}
// Posts collection
match /posts/{postId} {
allow read: if request.auth != null && (
resource.data.authorId == request.auth.uid || // Author can read
exists(/databases/$(database)/documents/follows/$(request.auth.uid)/$(resource.data.authorId)) // Follower can read
);
allow write: if request.auth != null && request.auth.uid == resource.data.authorId;
allow delete: if request.auth != null && request.auth.uid == resource.data.authorId;
}
// Comments collection
match /comments/{commentId} {
allow read: if true; // Public comments
allow write: if request.auth != null;
allow update: if request.auth != null && request.auth.uid == resource.data.userId;
allow delete: if request.auth != null && request.auth.uid == resource.data.userId;
}
}
}
Example 2: E-Commerce Product Catalog
Requirements:
- Products are publicly readable.
- Only store admins can create, update, or delete products.
- Customers can read their own orders.
- Only the system can create orders (no client writes).
Rules:
service cloud.firestore {
match /databases/{database}/documents {
// Products - public
match /products/{productId} {
allow read: if true;
allow write: if request.auth.token.admin == true;
}
// Orders - server-only creation
match /orders/{orderId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId;
allow create: if false; // Only server can create via Admin SDK
allow update: if false; // Orders are immutable
allow delete: if false;
}
// Order items
match /orders/{orderId}/items/{itemId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId;
allow write: if false;
}
}
}
Important: Order creation is handled server-side using the Firebase Admin SDK, ensuring clients cannot manipulate pricing or inventory.
Example 3: Private Messaging System
Requirements:
- Messages are only visible to participants in a conversation.
- Users can only send messages to conversations they’re part of.
- Messages cannot be edited or deleted.
Rules:
service cloud.firestore {
match /databases/{database}/documents {
// Conversations
match /conversations/{conversationId} {
allow read: if request.auth != null &&
(request.auth.uid in resource.data.participants);
allow write: if false; // Only system creates conversations
}
// Messages
match /conversations/{conversationId}/messages/{messageId} {
allow read: if request.auth != null &&
(request.auth.uid in get(/databases/$(database)/documents/conversations/$(conversationId)).data.participants);
allow create: if request.auth != null &&
request.auth.uid in get(/databases/$(database)/documents/conversations/$(conversationId)).data.participants;
allow update: if false;
allow delete: if false;
}
}
}
Here, we use the get() function to fetch the parent document’s data and verify the user is a participant. This ensures messages are only accessible to those in the conversation.
FAQs
Can Firestore rules prevent data deletion by admins?
No. Firebase Admin SDK operations bypass Firestore security rules entirely. Admins have full access to all data. If you need to restrict admin actions, implement application-level controls or audit logs, not Firestore rules.
Do Firestore rules support OR conditions?
Yes. Use the || operator. Example: if request.auth.uid == resource.data.ownerId || request.auth.token.admin == true
Can I use Firestore rules to limit the number of documents a user can create?
Not directly. Firestore rules cannot count documents in a collection. To enforce limits, use Cloud Functions to validate document counts before allowing writes, or use a counter document updated on each creation.
Why does my rule work in the emulator but fail in production?
Common causes:
- Custom claims haven’t propagated (cached tokens).
- Time drift between client and server (ensure device clocks are accurate).
- Rules syntax errors not caught locally.
- Missing index errors (rules and queries must align).
Always test with the same authentication state and data structure in both environments.
Are Firestore rules case-sensitive?
Yes. Field names, path segments, and string comparisons are case-sensitive. email ≠ Email.
How do I handle anonymous users?
Anonymous users have a valid request.auth.uid but no email or provider info. You can allow access to them using request.auth != null. However, consider converting them to permanent accounts after they perform key actions.
Can I use Firestore rules to restrict access by IP address?
No. Firestore rules do not have access to client IP addresses. For IP-based restrictions, use a Cloud Function as a proxy or implement a firewall at the network level.
What happens if I deploy broken rules?
Firestore rules are deployed atomically. If your rules contain syntax errors, the deployment fails and the previous rules remain active. Always test locally before deploying.
How do I reset Firestore rules to default?
In the Firebase Console, click “Reset Rules” in the Rules tab. This reverts to test mode: allow read, write: if true;. Never use this in production.
Conclusion
Writing secure and scalable Firestore rules is not a one-time task—it’s an ongoing discipline that must evolve with your application. The rules you write today will protect your data tomorrow, and the care you take in crafting them reflects the integrity of your entire system. By following the principles of least privilege, validating every input, organizing your rules logically, and testing relentlessly, you build not just a database, but a trustworthy foundation for your users.
Remember: security is not a feature. It’s the baseline. Whether you’re building a social platform, an e-commerce store, or a private messaging app, your users expect their data to be safe. Firestore rules give you the tools to deliver on that promise. Use them wisely, test them thoroughly, and review them regularly.
As you continue to develop, revisit this guide. Bookmark the official documentation. Engage with the community. And never assume—always verify. With the right approach, Firestore rules become not a burden, but a powerful ally in your quest to build secure, reliable, and user-centric applications.