How to Write Firestore Rules

How to Write Firestore Rules Google Firestore is a flexible, scalable NoSQL cloud database designed for mobile, web, and server development. Its real-time synchronization, offline support, and serverless architecture make it a top choice for modern applications. However, without proper security rules, your data is vulnerable to unauthorized access, data leakage, or malicious manipulation. Writing

Oct 30, 2025 - 13:40
Oct 30, 2025 - 13:40
 1

How to Write Firestore Rules

Google Firestore is a flexible, scalable NoSQL cloud database designed for mobile, web, and server development. Its real-time synchronization, offline support, and serverless architecture make it a top choice for modern applications. However, without proper security rules, your data is vulnerable to unauthorized access, data leakage, or malicious manipulation. Writing effective Firestore rules is not optional—it’s essential. These rules define who can read or write to your database, under what conditions, and how data must be structured. This tutorial provides a comprehensive, step-by-step guide to mastering Firestore rules, from foundational concepts to advanced patterns, ensuring your application remains secure, scalable, and reliable.

Firestore rules are written in a domain-specific language called the Firestore Security Rules Language. Unlike traditional SQL-based access control, Firestore rules are evaluated at the document level and are enforced on the server side, meaning clients cannot bypass them—even if they attempt to manipulate the client SDK. This makes them a critical layer of defense in your application’s architecture. Whether you’re building a social app, an e-commerce platform, or an enterprise SaaS tool, understanding how to write precise, efficient, and maintainable rules is a core skill for any developer working with Firestore.

This guide will walk you through every aspect of writing Firestore rules—from setting up your first rule to debugging complex scenarios. You’ll learn best practices for structuring rules, avoid common pitfalls, explore real-world examples, and leverage powerful tools to validate your logic. By the end, you’ll have the confidence to implement robust, production-grade security that protects both your users and your data.

Step-by-Step Guide

1. Understanding the Firestore Data Model

Before writing rules, you must understand how Firestore organizes data. Firestore stores data in documents, which are contained within collections. A document is a JSON-like object with key-value pairs, and each document has a unique ID. Collections are containers for documents and can be nested within other collections to create hierarchical structures.

For example:

  • users/{userId} — a document in the “users” collection
  • users/{userId}/posts/{postId} — a document in a subcollection called “posts” under a specific user

Rules are written to target specific paths in this hierarchy. You can write rules for entire collections, specific documents, or even subcollections. The path structure determines the scope of your rule. Always begin by mapping out your data model. Sketch out how your collections and subcollections relate. This will help you determine where to apply rules and avoid over-permissive access.

2. Accessing the Firebase Console

To begin writing rules, navigate to the Firebase Console. Select your project, then go to the “Database” section in the left-hand menu. You’ll see two tabs: “Cloud Firestore” and “Realtime Database.” Click on “Cloud Firestore.”

On the Cloud Firestore page, click the “Rules” tab. Here you’ll find the rule editor, which allows you to write, test, and deploy your security rules. By default, Firestore is in “test mode,” where rules allow read and write access to anyone. This is fine for development, but never use it in production.

Always switch to “lockdown mode” during development by setting rules to deny all access:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

match /{document=**} {

allow read, write: if false;

}

}

}

This ensures that you explicitly define permissions rather than accidentally leaving data exposed.

3. Writing Your First Rule

Let’s assume you’re building a blog application with a “posts” collection. Each post has fields: title, content, authorId, and createdAt. You want only authenticated users to create posts, and only the author to read or edit their own posts.

Start by defining a rule for the “posts” collection:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

match /posts/{postId} {

allow read, write: if request.auth != null && request.auth.uid == resource.data.authorId;

}

}

}

Let’s break this down:

  • match /posts/{postId} — applies to every document in the “posts” collection. The {postId} is a wildcard that captures the document ID.
  • request.auth != null — ensures the user is signed in. If no user is authenticated, access is denied.
  • request.auth.uid == resource.data.authorId — compares the authenticated user’s ID with the authorId field stored in the document. Only the owner can read or write.

Deploy the rules by clicking “Publish.” Now, only signed-in users who created a post can access it. Others—even authenticated users—will be denied access.

4. Handling Subcollections

Suppose each post has comments stored in a subcollection: posts/{postId}/comments/{commentId}. You want users to comment on any post, but only edit or delete their own comments.

Add a nested match block:

match /posts/{postId} {

allow read, write: if request.auth != null && request.auth.uid == resource.data.authorId;

match /comments/{commentId} {

allow read: if true; // Anyone can read comments

allow write: if request.auth != null && request.auth.uid == resource.data.authorId;

}

}

Notice that the read rule for comments is open to everyone. This makes sense—comments are public by design. But write is restricted to the comment’s author. The resource.data.authorId here refers to the authorId field within the comment document, not the parent post.

Important: Subcollection rules are independent of parent collection rules. A user can be denied access to a document but still have access to its subcollections if explicitly permitted. Always define rules for subcollections explicitly.

5. Using Built-in Functions and Variables

Firestore rules provide powerful built-in functions and variables to help you write precise logic:

  • request.auth — contains the authenticated user’s data (UID, email, tokens)
  • resource.data — the data of the document being read or written
  • request.resource — the data being written (for create/update operations)
  • exists() — checks if a document exists
  • isNewDocument() — returns true if the document is being created
  • time and date — for time-based access control

Example: Allow users to create a profile only if it doesn’t already exist:

match /users/{userId} {

allow create: if request.auth != null && request.auth.uid == userId && !exists(/databases/$(database)/documents/users/$(userId));

allow read, write: if request.auth != null && request.auth.uid == userId;

}

Here, create is allowed only if the user is authenticated, the document ID matches their UID, and the document doesn’t already exist. This prevents users from overwriting other profiles.

6. Validating Data Structure with Request.Resource

Rules aren’t just about access—they’re also about data integrity. Use request.resource to validate the structure of incoming data before allowing writes.

Example: Enforce that a post must have a title and content, and the title must be under 200 characters:

match /posts/{postId} {

allow create, update: if request.auth != null && request.auth.uid == resource.data.authorId &&

request.resource.data.title is string &&

request.resource.data.title.size() <= 200 &&

request.resource.data.content is string &&

request.resource.data.content.size() > 0 &&

request.resource.data.authorId == request.auth.uid &&

request.resource.data.createdAt == request.time;

}

Notice how we validate:

  • Types: is string
  • Length: size()
  • Required fields: content.size() > 0
  • Immutability: createdAt == request.time ensures the timestamp can’t be changed after creation

This prevents malformed or malicious data from entering your database.

7. Using Functions to Reuse Logic

As your rules grow, repetitive conditions become hard to maintain. Use functions to encapsulate logic:

function isAuthor() {

return request.auth != null && request.auth.uid == resource.data.authorId;

}

function isSignedIn() {

return request.auth != null;

}

match /posts/{postId} {

allow read: if isSignedIn();

allow write: if isAuthor();

}

match /posts/{postId}/comments/{commentId} {

allow read: if isSignedIn();

allow write: if isAuthor();

}

Functions improve readability and reduce duplication. They can also be unit-tested in the Firebase Emulator (covered later).

8. Testing Rules with the Firebase Emulator Suite

Never deploy rules without testing. The Firebase Emulator Suite allows you to simulate Firestore, Authentication, and other services locally.

Install the Firebase CLI:

npm install -g firebase-tools

Initialize your project:

firebase init

Select “Firestore” and “Emulators.” Start the emulators:

firebase emulators:start

Use the Emulator UI (available at http://localhost:4000) to:

  • Manually write test data
  • Simulate authenticated and unauthenticated requests
  • See real-time rule evaluation results

For automated testing, write unit tests using the firebase-admin SDK and the firebase-testing library. This lets you verify your rules behave as expected under various scenarios.

9. Deploying Rules

Once your rules are tested and working correctly, deploy them using the Firebase Console or CLI:

firebase deploy --only firestore:rules

Always deploy incrementally. Test in staging first. Use version control (Git) to track changes to your rules file. Treat your rules like code—review, test, and deploy them with the same rigor as your application logic.

Best Practices

1. Deny by Default, Allow Explicitly

Never rely on default permissions. Start with allow read, write: if false; and add permissions only when necessary. This principle of least privilege minimizes attack surfaces. Even if you’re building a public app, restrict write access to authenticated users only.

2. Avoid Using Wildcards in Sensitive Paths

Be cautious with {document=} or {collection=}. These match any document or collection in your database. A rule like:

match /{document=**} {

allow read: if true;

}

Exposes every document in your database to public read access. Always scope rules to specific collections or document paths.

3. Validate All Incoming Data

Client-side validation can be bypassed. Always validate data types, required fields, and constraints on the server using request.resource. For example, if a field should be a number, verify request.resource.data.score is number. If a string must be an email, use a regex:

request.resource.data.email matches /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/

4. Use Roles and Custom Claims for Complex Permissions

For applications with multiple roles (admin, moderator, user), use Firebase Authentication’s custom claims. Set claims during user creation (e.g., via Cloud Functions) and check them in rules:

function isAdmin() {

return request.auth.token.admin == true;

}

match /admin-panel/{document} {

allow read, write: if isAdmin();

}

This keeps logic clean and avoids embedding roles in document data, which can be tampered with.

5. Keep Rules Simple and Readable

Complex rules are hard to audit and debug. Break logic into functions. Use comments. Group related rules together. Avoid nesting too many conditions. If a rule exceeds 10 lines, consider refactoring.

6. Monitor Rule Violations

Use Firebase’s “Logs” tab in the console to monitor denied requests. Look for patterns—e.g., many failed writes to a specific path may indicate a client bug or an attack attempt. Set up alerts if possible.

7. Never Store Sensitive Data in Firestore Without Encryption

Firestore rules protect access, but not data at rest. Never store passwords, credit card numbers, or PII in plain text. Use client-side encryption or store sensitive data in a separate, more secure system like Firebase Realtime Database with stricter rules or Cloud Storage with signed URLs.

8. Update Rules with Versioning

Always include rules_version = '2'; at the top of your rules file. Version 2 supports nested matches and functions. Avoid version 1 unless maintaining legacy systems.

9. Test Edge Cases

Test scenarios like:

  • Unauthenticated users attempting to read/write
  • Users trying to modify other users’ data
  • Malformed data (e.g., sending a string instead of a number)
  • Large batches of writes
  • Deleted documents (use resource to check pre-deletion state)

10. Document Your Rules

Add comments explaining why a rule exists. For example:

// Only admins can delete users — prevents accidental or malicious removal

match /users/{userId} {

allow delete: if isAdmin();

}

This helps future developers (including yourself) understand intent, especially in team environments.

Tools and Resources

Firebase Emulator Suite

The Firebase Emulator Suite is the most critical tool for developing and testing Firestore rules. It runs locally and mirrors production behavior, allowing you to test authentication, data writes, and rule evaluations without affecting real data. It includes a web-based UI that visually shows which rules pass or fail for each request.

Firebase Console Rules Editor

The online editor in the Firebase Console provides syntax highlighting, basic validation, and a “Test Rules” panel. Use it to simulate requests with mock authentication states. While convenient, it’s not a substitute for the emulator for complex scenarios.

Firestore Rules Linter (Third-party)

Tools like rules-linter can be integrated into CI/CD pipelines to enforce code quality standards, detect anti-patterns, and ensure consistency across teams.

Firestore Rules Unit Testing Framework

The Firebase Testing Library allows you to write Jest or Mocha tests to validate rules programmatically. Example:

const { initializeTestApp, clearFirestoreData } = require('@firebase/rules-unit-testing');

const app = initializeTestApp({

projectId: 'my-project',

auth: { uid: 'user1' }

});

const db = app.firestore();

test('user can read their own document', async () => {

await db.collection('users').doc('user1').set({ name: 'Alice' });

const doc = await db.collection('users').doc('user1').get();

expect(doc.exists).toBe(true);

});

Automated testing ensures rules remain correct after refactoring.

Firestore Rules Documentation

Always refer to the official Firestore Security Rules documentation. It’s comprehensive, up-to-date, and includes examples for every use case.

Community Examples and Templates

GitHub repositories like Firebase Quickstart and Firebase Snippets provide real-world rule templates for common apps (chat, e-commerce, social networks). Use them as starting points—but always customize them to your data model.

VS Code Extensions

Install the “Firebase” extension for VS Code. It provides syntax highlighting, autocompletion, and quick access to Firebase documentation while editing rules files.

Real Examples

Example 1: Social Media App — User Posts and Likes

Structure:

  • /users/{userId} — profile data
  • /posts/{postId} — post content
  • /posts/{postId}/likes/{userId} — tracks who liked a post

Rules:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

// Users can read any profile, but only update their own

match /users/{userId} {

allow read: if true;

allow write: if request.auth != null && request.auth.uid == userId;

}

// Anyone can read posts, but only authenticated users can create

match /posts/{postId} {

allow read: if true;

allow create: if request.auth != null;

allow update, delete: if request.auth != null && request.auth.uid == resource.data.authorId;

}

// Anyone can view likes, but only authenticated users can like/unlike

match /posts/{postId}/likes/{likeId} {

allow read: if true;

allow write: if request.auth != null && request.auth.uid == likeId;

}

}

}

Explanation:

  • Public read access to posts encourages sharing.
  • Likes are stored as documents in a subcollection—each document ID is the user ID. This prevents duplicate likes.
  • Writing to /likes/{likeId} is only allowed if the authenticated user’s ID matches the like document ID. This ensures one like per user.

Example 2: E-Commerce — Product Listings and Orders

Structure:

  • /products/{productId} — product details
  • /orders/{orderId} — order data
  • /users/{userId}/orders/{orderId} — user-specific order history

Rules:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

// Products are publicly readable

match /products/{productId} {

allow read: if true;

allow write: if false; // Only admin can update via Cloud Functions

}

// Orders can only be created by authenticated users

match /orders/{orderId} {

allow create: if request.auth != null;

allow read: if request.auth != null && request.auth.uid == resource.data.userId;

allow update: if false; // Orders are immutable after creation

allow delete: if false;

}

// Users can read their own order history

match /users/{userId}/orders/{orderId} {

allow read: if request.auth != null && request.auth.uid == userId;

allow write: if false;

}

}

}

Explanation:

  • Products are read-only to prevent price manipulation.
  • Order creation requires authentication and includes the user ID in the document.
  • Orders are immutable after creation to preserve audit trails.
  • User-specific order history is accessible only to the owner.

Example 3: Chat Application — Private and Group Messages

Structure:

  • /chats/{chatId} — chat metadata
  • /chats/{chatId}/messages/{messageId} — message content
  • /chatMembers/{chatId}/{userId} — membership list

Rules:

rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

// Only members can read chat metadata

match /chats/{chatId} {

allow read: if exists(/databases/$(database)/documents/chatMembers/$(chatId)/$(request.auth.uid));

allow write: if false; // Only Cloud Functions create chats

}

// Only members can send messages

match /chats/{chatId}/messages/{messageId} {

allow read: if exists(/databases/$(database)/documents/chatMembers/$(chatId)/$(request.auth.uid));

allow create: if request.auth != null && exists(/databases/$(database)/documents/chatMembers/$(chatId)/$(request.auth.uid));

}

// Only admins can add/remove members

match /chatMembers/{chatId}/{userId} {

allow read: if request.auth != null && exists(/databases/$(database)/documents/chatMembers/$(chatId)/$(request.auth.uid));

allow write: if isAdmin(); // Custom claim

}

}

}

Explanation:

  • Chat access is controlled via a membership collection.
  • Using exists() ensures users can only access chats they’re part of.
  • Admins manage membership via Cloud Functions, not client-side writes.

FAQs

Can Firestore rules prevent data deletion?

Yes. Use allow delete: if false; to prevent deletion entirely, or restrict it to specific users or conditions (e.g., if request.auth.token.admin == true).

How do I handle user roles in Firestore rules?

Use Firebase Authentication custom claims (e.g., admin, moderator) set via Cloud Functions. Then check them in rules using request.auth.token.roleName. Never store roles in document data—they can be modified by clients.

Can I use Firestore rules to validate email format?

Yes. Use regex with matches:

request.resource.data.email matches /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/

This ensures only valid email addresses are accepted.

Do Firestore rules apply to batch writes?

Yes. Each operation in a batch is evaluated individually against the rules. If any operation fails, the entire batch is rejected.

Can I write rules that depend on data in other collections?

Yes. Use the exists() function to check for the existence of a document in another collection. For example:

allow write: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/subscriptions/active);

This allows writing only if the user has an active subscription.

How do I debug a rule that’s not working?

Use the Firebase Emulator UI. Simulate requests with mock authentication. Check the “Rules” tab to see which condition failed. Also, review the Firebase Console logs for denied access events.

What happens if I change rules while users are connected?

Firestore rules are evaluated in real time. When you deploy new rules, clients automatically receive the updated rules on their next network request. There’s no need to restart apps.

Are Firestore rules case-sensitive?

Yes. Field names, path variables, and string comparisons are case-sensitive. AuthorId is not the same as authorId.

Can I use Firestore rules to limit the number of documents a user can create?

Not directly. Firestore rules don’t support counting documents. Use Cloud Functions to monitor document counts and reject writes if limits are exceeded.

Do I still need server-side validation if I have Firestore rules?

Yes. Firestore rules protect the database, but server-side code (e.g., Cloud Functions) should validate data for business logic, send notifications, or trigger workflows. Rules are for access control; server code is for application logic.

Conclusion

Writing effective Firestore rules is a foundational skill for any developer building secure, scalable applications on Firebase. These rules are not just a technical formality—they are the gatekeepers of your data. A single misconfigured rule can expose sensitive information, enable data corruption, or open the door to abuse. By following the principles outlined in this guide—starting with a strict deny-all baseline, validating data rigorously, using functions to simplify logic, and testing exhaustively—you can build a security layer that is both robust and maintainable.

Remember: Firestore rules are code. Treat them with the same care as your application logic. Version them, review them, test them, and document them. Use the Firebase Emulator Suite religiously. Leverage custom claims for role-based access. Avoid wildcards unless absolutely necessary. And always, always assume that clients are hostile.

As your application grows, so will the complexity of your data model and access patterns. Start simple. Build incrementally. Refactor when needed. The investment you make today in writing clean, precise rules will pay dividends in security, user trust, and system reliability for years to come.

Mastering Firestore rules isn’t just about protecting data—it’s about building applications users can rely on. With the right approach, your Firestore database won’t just be powerful—it will be secure.