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:
- The client SDK sends a read or write request to Firestore.
- Firestore evaluates the request against your deployed rules.
- If a rule matches the path and its condition evaluates to
true, the operation is allowed. - 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 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 Authenticationrequest.auth.token— the full decoded ID token, including any custom claimsrequest.auth.token.email— the user’s email addressrequest.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");
}
}
}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.
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.
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 firestoreUse 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 firestoreRules 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
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.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.
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.Forgetting ownership checks on writes. A rule that only checks
request.auth != nullfor writes means any authenticated user can overwrite any document at that path. Check thatrequest.auth.uidmatches an owner field stored at create time, typicallyownerIdoruserId.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.
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.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.
Key takeaways
- Security rules live in
firestore.rules, run on Google’s infrastructure, and apply to client SDK access only - The default is deny. Rules must explicitly allow each operation. A new database in test mode ships with a permissive rule that must be replaced before production
- Admin SDK, Cloud Run, Cloud Functions, and service accounts bypass rules entirely. Enforce access control in server-side code for those paths
request.auth.uidis the key field for ownership checks;request.auth.tokenprovides custom claims for role-based access at no extra costresource.datais the existing document;request.resource.datais the incoming write data. Use both for precise field-level validation- Prefer custom auth claims over
get()for role checks. Claims are free;get()calls are billed reads - Test rules with the Firebase Emulator Suite before every deployment. Cover both allow and deny cases
- Rules are not query filters. A query is either fully allowed or fully denied
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.