Firebase Security Rules: A Practical Guide for Developers
Full Stack Developer
Firebase makes it incredibly easy to build and launch applications. You can have authentication working in minutes, connect a React application to Firestore, up…
Firebase makes it incredibly easy to build and launch applications.
You can have authentication working in minutes, connect a React application to Firestore, upload files to Cloud Storage, and have a functional backend without provisioning a traditional server.
That simplicity is one of Firebase's biggest strengths.
It can also become one of its biggest weaknesses.
The moment your application starts storing real user data, the question changes from:
"Can my application access this data?"
to:
"Who exactly should be allowed to access this data, and under what conditions?"
That is where Firebase Security Rules come in.
Security Rules are the layer that sits between your application and Firebase resources, determining whether a request should be allowed or rejected. They can be simple enough for a beginner to understand, but powerful enough to implement fairly sophisticated authorization systems.
This guide explains Firebase Security Rules from the ground up, with examples aimed at both developers and people who simply want to understand how their Firebase application is protected.
What Are Firebase Security Rules?
Think of your Firebase database like an apartment building.
Your application is the person trying to enter the building.
Your database contains the apartments.
Authentication answers:
"Who are you?"
Security Rules answer:
"What are you allowed to do?"
For example, imagine an application where users have profiles:
/users/alice
/users/bob
/users/charlieYou might want Alice to be able to read and update her own profile, but not Bob's.
Authentication can tell Firebase:
Alice's UID = abc123But authentication alone doesn't automatically mean Alice should be allowed to modify every document in your database.
Your Security Rules can enforce:
If the person making the request owns this document → allow.
Otherwise → deny.That distinction between identity and permission is one of the most important concepts to understand when building secure Firebase applications.
Authentication Is Not Authorization
This is where many developers get confused.
Suppose you have this rule:
allow read, write: if request.auth != null;It means:
Anyone who is authenticated can read and write this data.
It does not mean:
Users can only access their own data.
Those are completely different security models.
Consider an application with 10,000 users.
If your rule is:
allow read, write: if request.auth != null;then User A may be able to access User B's data simply because both users are authenticated.
Authentication establishes identity.
Authorization establishes permission.
A secure application generally needs both.
Firebase Security Rules can use Firebase Authentication information, including the authenticated user's UID and custom claims, when determining whether to allow a request.
Understanding the Basic Structure
For Cloud Firestore, a Security Rules file generally starts like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Your rules go here
}
}The important pieces are:
rules_version
This specifies the rules language version.
For modern Firestore rules, you will commonly see:
rules_version = '2';Version 2 is particularly important when working with collection group queries and recursive wildcards.
service
This tells Firebase which service the rules apply to.
For Firestore:
service cloud.firestorematch
match defines which documents a rule applies to.
For example:
match /users/{userId}means:
Apply these rules to documents inside the
userscollection.
allow
allow determines whether a specific operation is permitted.
For example:
allow read: if request.auth != null;means authenticated users can read the matching documents.
The Most Important Firebase Security Rule You'll Learn
Let's start with a simple user-profile example.
Suppose your database looks like this:
users
├── user123
├── user456
└── user789Each document represents one user.
You want users to access only their own documents.
You could write:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
}Let's break this down.
request.auth != nullmeans:
The user must be authenticated.
Then:
request.auth.uid == userIdmeans:
The authenticated user's UID must match the ID of the document they're trying to access.
So if Alice's UID is:
alice123and she requests:
/users/alice123the rule passes.
But if she tries:
/users/bob456the rule fails.
That's authorization.
read and write Are Not the Whole Story
Firestore allows you to be more precise than simply saying read or write.
You can separate operations into:
allow get:
allow list:
allow create:
allow update:
allow delete:This becomes useful when your application needs different permissions for different operations.
For example:
match /posts/{postId} {
allow get: if true;
allow list: if true;
allow create: if request.auth != null;
allow update: if request.auth.uid == resource.data.authorId;
allow delete: if request.auth.uid == resource.data.authorId;
}Now you have much more control over what users can do.
For example:
Anyone can read a post.
Only authenticated users can create posts.
Only the author can modify their post.
Only the author can delete their post.
That is far more useful than simply opening the entire collection to authenticated users.
The Difference Between resource and request.resource
This is one of the concepts that developers often struggle with when learning Firestore Security Rules.
Here's the simple way to remember it:
resource
Think:
The data that already exists.
request.resource
Think:
The data that someone is trying to create or save.
Imagine you have:
{
"name": "Victor",
"role": "user"
}A user attempts to change it to:
{
"name": "Victor",
"role": "admin"
}If you want to prevent users from changing their own role, you need to inspect the incoming document.
That's where request.resource becomes useful.
For example:
allow update: if request.auth.uid == userId
&& request.resource.data.role == resource.data.role;This says:
The user can update their own document, but they cannot change the
rolefield.
This is extremely useful for protecting fields such as:
roleisAdminaccountStatusverifiedsubscriptionTiercreatedAt
ownership fields
Protecting Fields
Imagine your application has:
{
"name": "Victor",
"email": "victor@example.com",
"role": "user",
"credits": 100
}You might allow the user to update:
name
emailbut not:
role
creditsOne approach is to explicitly compare the protected fields:
allow update: if request.auth.uid == userId
&& request.resource.data.role == resource.data.role
&& request.resource.data.credits == resource.data.credits;This ensures that those values cannot be changed through that client operation.
There is an important limitation to understand, though: Firestore reads happen at the document level. Security Rules cannot simply hide individual fields from a document during a read. If some fields need different access levels, a better architecture is often to place sensitive information in a separate document or subcollection with stricter rules.
For example:
/users/{userId}
name
email
avatar
/users/{userId}/private/security
role
internalNotes
billingInformationNow you can protect the private data separately.
Validating Data Before It Gets Written
Security Rules aren't only about deciding who can access something.
They can also help validate what users are attempting to write.
Suppose users can create posts:
{
"title": "My First Post",
"body": "Hello world"
}You could require:
allow create: if request.auth != null
&& request.resource.data.title is string
&& request.resource.data.body is string;Now the incoming document must contain the expected types.
You can go further.
For example:
allow create: if request.auth != null
&& request.resource.data.title is string
&& request.resource.data.title.size() > 0
&& request.resource.data.body is string;This gives you an additional layer of protection against malformed writes.
The important mindset is:
Don't only ask whether the user is allowed to write. Ask whether the data they're trying to write is valid.
Role-Based Access Control
Now let's move into a more realistic application.
Imagine an estate management platform like EazyPass.
You could have:
resident
security
admin
superadminThese users should not have the same permissions.
A resident might be able to:
Create visitor passes
View their own visitors
Report an issue
A security officer might be able to:
Verify visitor passes
Record entries
View access logs
An administrator might be able to:
Manage residents
Manage security personnel
View estate-wide records
This is a classic example of role-based access control, or RBAC.
Using Custom Claims for Roles
Firebase Authentication supports custom claims that can be used for access control.
For example, a user's authentication token might contain:
{
role: "admin"
}Then your Security Rule can check:
request.auth.token.role == "admin"For example:
match /adminData/{document} {
allow read, write: if request.auth != null
&& request.auth.token.role == "admin";
}Now only users with the appropriate claim can access the data.
Custom claims are intended for access-control information, not for storing arbitrary profile data. Firebase also limits the size of custom claims, so large amounts of user information should live elsewhere.
Don't Trust the Frontend
This deserves its own section.
Suppose your React application contains:
if (user.role === "admin") {
showAdminDashboard();
}That's useful for the user interface.
But it is not security.
A malicious user can potentially manipulate frontend code, make direct requests, or interact with Firebase outside the UI.
Your frontend should decide:
What should the user see?
Your Security Rules should decide:
What is the user actually allowed to do?
That's why security logic must exist at the Firebase security layer rather than only inside your React components.
Firebase specifically separates Security Rules from application logic so that client-side bugs don't become the sole protection for your data.
One of the Biggest Firebase Mistakes
You may have seen rules like this during development:
allow read, write: if true;It is convenient.
It makes development incredibly easy.
And it is dangerous in production.
That rule effectively says:
Anyone can access this data.
Firebase's documentation explicitly warns against leaving broad access rules in production. Test-mode configurations are useful during development, but production applications should have rules appropriate to their actual authorization model.
A better development habit is:
Start locked.
Open only what you need.
Test it.
Deploy.
Review it again.Don't start with:
Open everything.
Build the entire application.
Hopefully remember to secure it later.Security should be part of the architecture from the beginning.
The "Rules Are Not Filters" Problem
This is one of the most important Firestore concepts.
Imagine you have:
match /posts/{postId} {
allow read: if resource.data.published == true;
}You might think this query should work:
db.collection("posts").get();and Firestore would simply return the published posts.
It doesn't work that way.
Security Rules are not filters.
Firestore evaluates whether the query could potentially return documents that the user isn't authorized to read. If the query could return an unauthorized document, the entire query can fail.
So if your rule requires:
resource.data.published == trueyour query should include the corresponding constraint:
db.collection("posts")
.where("published", "==", true)
.get();Think of it this way:
Your query needs to prove that its potential result set satisfies your Security Rules.
This is a critical concept when designing Firestore applications.
Security Rules and Queries Must Agree
Consider a rule:
match /stories/{storyId} {
allow read: if request.auth != null
&& resource.data.authorId == request.auth.uid;
}Then this query:
db.collection("stories").get();can fail because it could potentially return stories belonging to other users.
Instead, your query should reflect the same authorization constraint:
db.collection("stories")
.where("authorId", "==", user.uid)
.get();The rule and the query are now aligned.
This relationship between your data model, Security Rules, and queries is something you should think about before building your Firestore collections.
Using get() and exists() for More Complex Authorization
Sometimes the information required to make an authorization decision isn't inside the document being accessed.
Imagine:
/users/{userId}contains:
{
"estateId": "estate123"
}and you have:
/estates/estate123/members/{userId}You may want to check whether a user belongs to a particular estate before granting access.
Firestore Security Rules can use functions such as:
get()and:
exists()to access other Firestore documents when evaluating conditions.
This allows you to build more sophisticated authorization systems.
But don't turn your rules into a giant application backend.
Complex rules become harder to reason about, test and maintain.
A good rule is:
Keep your authorization model understandable.
If you need increasingly complicated logic, revisit your data model.
Reusable Security Functions
As your application grows, you'll quickly notice repeated conditions.
For example:
request.auth != nullmight appear everywhere.
Instead of repeating complex authorization logic, you can create reusable functions.
For example:
function signedIn() {
return request.auth != null;
}Then:
allow read: if signedIn();You could also create:
function isAdmin() {
return request.auth != null
&& request.auth.token.role == "admin";
}Then:
allow write: if isAdmin();Firebase Security Rules support custom functions specifically to make more complex rules easier to maintain. There are limitations to these functions, so they should remain focused rather than becoming miniature application programs.
Protecting Firebase Storage
Security isn't only about Firestore.
If your application allows users to upload:
Profile pictures
Documents
Videos
Product images
Private files
you also need to secure Firebase Cloud Storage.
For example, if each user's files are stored under:
/users/{userId}/...you might make access dependent on the authenticated user's UID.
The principle is the same:
Who is making the request?
↓
What resource are they requesting?
↓
Are they allowed to perform this operation?Firebase Security Rules support Cloud Storage as well as Cloud Firestore and Realtime Database, although the rule syntax and available capabilities differ between products.
Be Careful With Server-Side Code
Here's another important distinction.
If you're using a server environment with Firebase Admin SDK or server client libraries, don't assume your Firestore Security Rules are protecting those server-side operations.
Firebase's documentation notes that server client libraries bypass Firestore Security Rules and instead authenticate through Google Application Default Credentials and IAM.
This means you have two different security environments:
Client-side Firebase SDK
React
↓
Firebase SDK
↓
Authentication
↓
Firestore Security Rules
↓
DatabaseServer-side/Admin access
Node.js server
↓
Admin / server client
↓
Google credentials / IAM
↓
FirestoreThis distinction becomes extremely important when you're building a hybrid architecture with React, Firebase and Node.js.
A Practical Secure Firestore Example
Let's put several ideas together.
Imagine a blog application.
Each post contains:
{
"title": "My Article",
"body": "Article content...",
"authorId": "abc123",
"published": true,
"createdAt": "timestamp"
}You might want:
Anyone to read published posts.
Authors to read their own unpublished posts.
Authenticated users to create posts.
Authors to update their posts.
Authors to delete their posts.
Nobody to change the original author.
A conceptual ruleset could look like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isAuthor() {
return signedIn()
&& request.auth.uid == resource.data.authorId;
}
match /posts/{postId} {
allow get: if resource.data.published == true
|| isAuthor();
allow list: if resource.data.published == true;
allow create: if signedIn()
&& request.resource.data.authorId == request.auth.uid;
allow update: if isAuthor()
&& request.resource.data.authorId == resource.data.authorId;
allow delete: if isAuthor();
}
}
}This is only an example, not a universal production ruleset. Your actual rules should be designed around your data model, user roles and application behavior.
The important thing is the thinking behind it:
Identity → ownership → operation → data validation.
Testing Your Rules
Writing Security Rules isn't enough.
You need to test them.
A rule can look perfectly reasonable and still block legitimate queries or accidentally allow an operation you didn't intend.
Firebase recommends using the Firebase Local Emulator Suite for testing rules before deploying them to production.
A useful test plan should include both allowed and denied scenarios.
For example:
ScenarioExpectedUnauthenticated user reads private profile. ❌ Denied
User reads own profile ✅ Allowed
User reads another user's private profile ❌ Denied
User updates own name. ✅ Allowed
User changes own role. ❌ Denied
Admin updates permitted record ✅ Allowed
User attempts to delete another user's record ❌ Denied
The denied tests are just as important as the successful ones.
In fact, they are often more important.
A Security Checklist for Firebase Developers
Before deploying a Firebase application, ask yourself:
Authentication
Are protected resources limited to authenticated users?
Do I know exactly which users should have access?
Authorization
Can users access only the documents they should?
Are admin permissions explicitly controlled?
Are roles enforced on the backend/security layer?
Writes
Can users modify fields they shouldn't?
Can users change ownership fields?
Are incoming values validated?
Queries
Do my queries satisfy my Security Rules?
Have I tested collection queries as well as individual document reads?
Storage
Are uploaded files protected?
Can users access another user's files?
Server-side access
Am I using Admin SDK/server libraries?
If so, have I configured appropriate IAM and server-side authorization?
Testing
Have I tested unauthorized requests?
Have I tested different user roles?
Have I tested create, read, update and delete separately?
Production
Did I remove development-only open rules?
Have I reviewed my rules before deployment?
Common Mistakes to Avoid
1. Making Everything Public
allow read, write: if true;Convenient during development.
Dangerous in production.
2. Assuming Authentication Means Security
allow read, write: if request.auth != null;This only verifies that someone is signed in.
It doesn't establish ownership or role.
3. Securing Only the UI
Hiding an admin button doesn't make the underlying data secure.
Security must be enforced where the data is accessed.
4. Ignoring Queries
A rule may work perfectly for an individual document but fail when your application runs a collection query.
Remember:
Rules are not filters.
5. Storing Sensitive Data Together
If different users require different access to different pieces of information, consider separating sensitive data into different documents or subcollections.
6. Making Custom Claims a User Database
Custom claims are excellent for access-control information.
They're not a replacement for your user database.
Keep profile and application data in your database, and use claims for authorization-related information.
The Bigger Lesson
Firebase Security Rules aren't something you add to your application at the very end.
They should influence how you design your application from the beginning.
When you create a collection, ask:
Who owns this data?
When you create a field, ask:
Who can change this?
When you create a query, ask:
Who should be able to retrieve these documents?
When you create a role, ask:
What can this role actually do?
And when you create a backend endpoint, ask:
Is this operation protected by the correct server-side authorization mechanism?
These questions turn security from an afterthought into part of your architecture.
Final Thoughts
Firebase removes a lot of backend infrastructure work, but it doesn't remove the responsibility of designing secure applications.
That's the beauty of Security Rules.
You don't need to build a traditional authorization server just to protect a Firestore collection. You can express access policies close to the data and make those policies part of your application's architecture.
But powerful doesn't mean automatic.
A secure Firebase application requires you to understand:
Authentication — Who is the user?
Authorization — What is the user allowed to do?
Validation — Is the data they're submitting valid?
Data modeling — Is the database structured in a way that makes access control straightforward?
Query design — Does the query respect the same constraints as the rules?
Testing — Have you proved that both legitimate and malicious requests behave correctly?
Once these pieces start working together, Firebase becomes much more than a convenient backend.
It becomes a platform you can use to build applications that are not only fast to develop, but also designed with security in mind.
And that is the difference between an application that works and an application that is ready to be trusted.
Quick Reference
Authentication
↓
Who are you?
Security Rules
↓
What are you allowed to do?
Data Validation
↓
Is what you're submitting acceptable?
Database Structure
↓
Is the data organized for secure access?
Query Design
↓
Does the query satisfy the rules?
Testing
↓
Can we prove unauthorized access is blocked?
Production
↓
Deploy with confidenceThe simplest principle to remember is this:
Never ask only, "Can this user access Firebase?" Ask, "Exactly what should this user be allowed to do?"
That's where good Firebase security begins.