Firestore Security Rules Explained: Access Control, Examples, and Best Practices

Firestore Security Rules control which documents client SDKs can read or write. They run on Google’s servers, evaluate before any data is returned or written, and cannot be bypassed by a client. One thing to understand from the start: rules only apply to client SDK access. Code using the Admin SDK or a service account bypasses them entirely. This is by design, not by accident.

What Firestore Security Rules actually do

When a mobile or web app talks directly to Firestore using the client SDK, every read and write request goes through a security rules evaluation. The rules sit between your client and the database. Before Firestore returns any data or commits any write, it checks whether the current request matches a rule that permits it.

A useful way to think about it: imagine a nightclub with a bouncer at the door. The bouncer checks every person’s ID before they can enter. They do not go inside and decide which tables to serve. They make a single decision at the entrance: in or out. Security rules work the same way. They evaluate once per request, at the point of access, and either allow or deny the whole thing. They do not partially serve a query or hide individual documents from a result set.

This matters because client apps are not trusted the same way your backend code is. Anyone can inspect a mobile app or browser app and find the Firestore paths it uses. Without security rules, any user with your project ID could attempt to read or write documents directly. Security rules are your primary enforcement layer for all client-facing Firestore access.

Rules run on Google’s infrastructure as part of Firestore itself, not inside your app. A client cannot modify, disable, or work around them. That is the property that makes them worth relying on.

How Firestore Security Rules work

The evaluation flow for every client SDK request:

  1. The client SDK sends a read or write request to Firestore.
  2. Firestore evaluates the request against your deployed rules.
  3. If a rule matches the path and its condition evaluates to true, the operation is allowed.
  4. If the condition is false, or no rule matches, the operation is denied. No data is returned or written.

The default is deny. There is no rule that permits anything unless you write one. A new Firestore database in test mode ships with allow read, write: if true; as a convenience for prototyping, but that rule must be replaced before handling real user data.

Rules are gatekeepers, not filters

Rules evaluate once and make an all-or-nothing decision. A query either runs in full or is denied entirely. If a rule would deny access to any single document a query might return, the entire query is rejected. No partial results are returned, no documents are silently hidden. Design your queries so they only request documents the authenticated user is already allowed to read. See Firestore Queries for how query design and rules interact.

Rules structure

Security rules are written in a custom language that resembles JavaScript. They live in a file called firestore.rules and are deployed alongside your application using the Firebase CLI.

The top-level structure is always the same: a rules_version declaration, a service cloud.firestore block, and inside that, a root match /databases/{database}/documents block. Everything you write goes inside that root block.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    // Deny everything by default — only what matches below is allowed
    match /users/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == userId;
    }

  }
}

Each match rule targets a path. Wildcard segments like {userId} capture document IDs as variables that you can reference in conditions. The operations you can control are: read, write, get, list, create, update, and delete.

read covers both get (single document fetch) and list (query). write covers create, update, and delete. Separating them is often useful: you might allow any authenticated user to create a document, while only the document owner can update or delete it.

Using request.auth

The request.auth object describes the user making the request. It is null for unauthenticated requests. Always check request.auth != null before accessing any property on it. Skipping this check is a common source of rules evaluation errors.

  • request.auth.uid — the user’s unique identifier from Firebase Authentication
  • request.auth.token — the full decoded ID token, including any custom claims
  • request.auth.token.email — the user’s email address
  • request.auth.token.email_verified — whether the email has been verified

The most common pattern is ownership: check that the user’s UID matches a field stored on the document. The example below also separates create from update and delete, which lets you express different conditions for each operation.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    match /profiles/{userId} {
      // Any signed-in user can read any profile
      allow read: if request.auth != null;

      // Only the profile owner can write their own profile
      allow write: if request.auth.uid == userId;
    }

    match /posts/{postId} {
      // Anyone can read posts — no authentication required
      allow read: if true;

      // Only users with a verified email can create a post
      allow create: if request.auth != null
                    && request.auth.token.email_verified == true;

      // Only the original author can update or delete their post
      allow update, delete: if request.auth.uid == resource.data.authorId;
    }

  }
}

The posts rules do three different things. Reads are public. Creates require a verified email, which reduces spam from throwaway accounts. Updates and deletes check the existing document’s authorId field against the requesting user’s UID. Any authenticated user can create a post, but only the author can change or remove it.

Using resource.data and request.resource.data

Inside a rule, you have access to two document objects:

  • resource.data — the document’s current state in Firestore. Available for read, update, and delete operations.

  • request.resource.data — the incoming data being written. Available for create and update operations.

Together, they let you enforce field-level constraints. You can check that a required field is present, that a value falls within acceptable bounds, or that a user is not changing a field they should not touch.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    match /orders/{orderId} {
      // Only the order owner can read their order
      allow read: if request.auth.uid == resource.data.ownerId;

      // Allow create only if the new document sets ownerId to the requester's UID
      // This prevents one user from creating an order attributed to someone else
      allow create: if request.auth != null
                    && request.resource.data.ownerId == request.auth.uid
                    && request.resource.data.status == "pending";

      // Allow update only if the owner is making the change
      // and is not trying to soft-delete the order through the client SDK
      allow update: if request.auth.uid == resource.data.ownerId
                    && request.resource.data.status != "deleted";
    }

  }
}

The create rule validates two things at once: the requester must set themselves as the owner (preventing impersonation), and new orders can only start in pending status. The update rule reads resource.data.ownerId to confirm who owns the existing document, then checks request.resource.data.status to block a soft-delete workaround. Status transitions that should only happen server-side belong in your backend code, not in client rules.

For background on how Firestore data is structured and how fields map to documents, see the Firestore Data Model guide.

Custom functions

Rules support user-defined functions to avoid repeating the same checks across multiple match blocks. Define them inside the service block, then call them from your conditions.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }

    function isOwner(docOwnerId) {
      return isSignedIn() && request.auth.uid == docOwnerId;
    }

    // Reads the "role" field from a custom claim set via the Admin SDK.
    // Custom claims are free — no extra Firestore reads, no added latency.
    function hasRole(role) {
      return isSignedIn() && request.auth.token.role == role;
    }

    match /articles/{articleId} {
      allow read: if isSignedIn();
      allow create: if isSignedIn();
      allow update, delete: if isOwner(resource.data.authorId) || hasRole("editor");
    }

  }
}
Prefer custom claims over document lookups for roles

The hasRole function above reads from a custom claim (request.auth.token.role) set by your server using the Firebase Admin SDK. Claims are embedded in the auth token itself, so reading them in a rule costs nothing and adds no latency. Compare this to a get() call, which triggers a billed Firestore read on every request that evaluates the rule. For role-based access control, custom claims are almost always the better choice.

See Firestore Overview for context on when to use the Admin SDK vs the client SDK.

When to use Firestore Security Rules

Security rules are the right tool when your app talks directly to Firestore from a mobile or web client using the Firebase SDK. They fit naturally for:

  • Per-user data. User profiles, saved preferences, and draft content that belongs to one person and should not be visible to others.

  • Public read, restricted write. A blog where anyone can read posts but only authenticated authors can create or edit them.

  • Owner-gated documents. Orders, invoices, or private messages where the requester must match the document’s owner field.

  • Role-based access with custom claims. Admin or moderator users who have elevated permissions encoded in their auth token.

  • Field-level write validation. Enforcing that a required field is present, a price is positive, or a status value belongs to an allowed set.

  • Multi-user apps with document ownership. Shared notes, collaborative lists, or any scenario where different users own different documents within the same collection.

If you are unsure whether Firestore is the right database for your use case, the Cloud SQL vs Firestore comparison covers when each option makes more sense.

When Firestore Security Rules do not apply

Security rules only apply to the Firebase client SDKs (web, iOS, Android, Flutter). They do not apply to any of the following:

  • Firebase Admin SDK. The Admin SDK is initialised with a service account and is considered fully trusted. It has unrestricted read and write access to Firestore regardless of your rules.

  • Cloud Run and Cloud Functions. Backend services that use the Admin SDK or Application Default Credentials bypass rules completely. A Cloud Run service or a Cloud Function reading from Firestore is not subject to your security rules.

  • Service accounts. Any service account granted Firestore access via IAM bypasses rules. IAM controls what GCP identities can do at the platform level. Security rules control what Firebase client users can do at the document level. The two systems are independent.

  • Server-side client libraries. The GCP Cloud Firestore client libraries for Node.js, Python, Go, Java, and other server languages authenticate with ADC or a service account and bypass rules.

Server-side code must enforce its own access control

Bypassing rules for server-side code is intentional, but it means your Cloud Run services and Cloud Functions have full, unrestricted write access to Firestore by default. If your backend handles requests from multiple users, it must check who the caller is and what they are allowed to do before touching the database. Rules will not catch anything from that access path.

Firestore Security Rules vs IAM vs server-side authorisation

Three different mechanisms control access to your Firestore data. They operate at different levels and serve different purposes. Understanding all three prevents the most common security mistakes in Firestore-backed apps.

Firestore Security Rules control document-level access for Firebase client SDK users. They have access to the requester’s Firebase Authentication identity (via request.auth), the path being accessed, and the document’s data. They are the right tool for securing what client apps can do.

IAM controls which GCP identities (users, groups, service accounts) can interact with GCP resources at the platform level. Granting a service account the roles/datastore.user role lets it read and write Firestore with no rules evaluation at all. IAM operates above the rules layer. See What is IAM for an introduction to how GCP identity and access management works.

Server-side authorisation logic is the access control you implement in your own backend code. When a Cloud Run service handles a user request, it should validate who the caller is and what they are allowed to do before touching Firestore. This is standard application-level authorisation: checking JWT claims, verifying document ownership, enforcing business rules. It has nothing to do with Firestore Security Rules.

In a typical production app, all three work together. IAM grants your backend services access to Firestore. Your backend enforces its own authorisation logic before reading or writing. Firestore Security Rules protect the client-facing access path. None of them is a substitute for the others.

For broader guidance on layering these controls in a real application, see Securing Production Systems.

Reading other documents in rules

Rules can look up other documents using get() and exists(). This is sometimes used to check a roles document before deciding whether to allow access.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    // Read a roles document to check admin status.
    // Warning: each get() call is a billed Firestore read.
    function isAdmin() {
      let roleDoc = /databases/$(database)/documents/roles/$(request.auth.uid);
      return exists(roleDoc) && get(roleDoc).data.role == "admin";
    }

    match /adminContent/{docId} {
      allow read, write: if isAdmin();
    }

  }
}

This works, but every request that evaluates isAdmin() triggers a billed Firestore document read. On a high-traffic collection, this can double your read costs.

When to use custom claims instead

For most role checks, custom auth token claims are a better fit. Set the user’s role via the Firebase Admin SDK when the token is issued. The role is then available in request.auth.token.role with no latency and no cost, because it is part of the token itself. Reserve get() for situations where the access decision genuinely depends on data that cannot be encoded into a token claim, such as team membership stored in a shared document.

Testing and deploying rules

A rules mistake can go in two directions. Overly permissive rules expose data. Overly restrictive rules break the app. Both are serious in production, and neither is obvious until something goes wrong. Test before deploying.

The Firebase Emulator Suite runs a local Firestore instance with full security rules support. Start it with:

firebase emulators:start --only firestore

Use the @firebase/rules-unit-testing library to write automated tests. Each test should cover at least one allow case and one deny case for every access pattern in your rules.

const { initializeTestEnvironment, assertFails, assertSucceeds } = require('@firebase/rules-unit-testing');
const fs = require('fs');

const testEnv = await initializeTestEnvironment({
  projectId: 'my-project',
  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }
});

// Authenticated user reading their own profile — should succeed
const authedUser = testEnv.authenticatedContext('user-abc');
await assertSucceeds(authedUser.firestore().doc('users/user-abc').get());

// Authenticated user reading someone else's profile — should fail
await assertFails(authedUser.firestore().doc('users/other-user').get());

// Unauthenticated request — should fail
const guest = testEnv.unauthenticatedContext();
await assertFails(guest.firestore().doc('users/user-abc').get());

Once your tests pass, deploy using the Firebase CLI:

# Deploy Firestore rules only
firebase deploy --only firestore:rules

# Deploy rules and indexes together
firebase deploy --only firestore
No gradual rollout

Rules take effect within seconds of deployment and apply globally to all client SDK connections at once. A broken rule affects every user immediately. This is why testing with the emulator before deploying is not optional for production apps.

The Firebase console also includes a Rules Playground for quick interactive testing against your live database. It is useful for verifying a single specific scenario, but it is not a substitute for automated tests that cover your full allow and deny surface area.

Common mistakes

  1. Leaving test mode rules in production. Test mode ships with allow read, write: if true;, which gives any client complete access to your entire database. This rule expires after 30 days. When it does, access is denied by default and your app breaks in a different direction. Replace test mode rules with proper rules before you handle any real user data.

  2. Expecting rules to protect Admin SDK access. Cloud Run services, Cloud Functions, and any backend using the Firebase Admin SDK or GCP service account credentials bypass rules entirely. This is by design. If your server code needs restricted access to certain documents, enforce that restriction in your server code. Rules will not do it for you.

  3. Writing an overly broad wildcard allow rule. A rule like allow read, write: if request.auth != null; at the root of your document tree grants every signed-in user read and write access to everything in your database. This is nearly as dangerous as test mode. Scope your rules to specific paths and specific operations.

  4. Forgetting ownership checks on writes. A rule that only checks request.auth != null for writes means any authenticated user can overwrite any document at that path. Check that request.auth.uid matches an owner field stored at create time, typically ownerId or userId.

  5. Assuming rules act as query filters. Rules do not hide individual documents from query results. A query is either fully allowed or denied. If a user queries a collection and any document in the result set is protected, the entire query fails. Structure queries to only request documents the user is allowed to read.

  6. Using get() for every role check. Each get() call in a rule is a billed read. On a collection that handles hundreds of requests per minute, this adds up quickly. Use custom auth token claims for role checks wherever possible. They are set once via the Admin SDK and cost nothing to read in rules.

  7. Not testing rules before deploying. A typo in a field name, a missing null check on request.auth, or an incorrect path wildcard can silently break access for real users. Write tests using the Firebase rules unit testing library and run them before every deployment. If you hit unexpected denials in production, the permission denied errors guide covers how to diagnose them.

Frequently asked questions

Do Firestore Security Rules apply to the Admin SDK?

No. Rules only apply to access from the mobile and web client SDKs. The Firebase Admin SDK and any server-side code using a service account bypass rules completely. This is intentional — server-side code is considered trusted. You are responsible for implementing your own access control logic in that code.

Are Firestore Security Rules evaluated before data is returned?

Yes. Rules run on Google infrastructure before any data is read or written. If a rule condition evaluates to false, or no rule matches the requested path, the operation is denied and no data is returned to the client.

Can Firestore Security Rules filter query results?

No. Rules are not filters. A query either runs in full or is denied entirely. If a rule would deny access to any single document a query might return, the entire query fails. Design your queries to only request documents the current user is already allowed to read.

What is the difference between Firestore Security Rules and IAM?

Firestore Security Rules control which documents Firebase client SDK users can read or write, based on their Firebase Authentication identity. IAM controls which GCP identities — users, groups, service accounts — can access GCP resources at the platform level. A service account granted Firestore access via IAM bypasses security rules entirely.

How should I test Firestore Security Rules before deploying to production?

Use the Firebase Emulator Suite with the @firebase/rules-unit-testing library. Write tests that cover both allow and deny cases — a rules mistake can expose data to the wrong users or lock legitimate users out. Run tests before every rules deployment using the Firebase CLI.

Last verified: 23 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.