Mastering Firestore Security Rules

Mastering Firestore Security Rules

Learn the architectural patterns of Firestore security rules, firestore rules syntax and examples for authenticated users, a guide for founders, hackers and newbie devs

Muhammad Hassaan
Muhammad HassaanMH Labs
8 min read

Firestore security rules are your database's bouncer. They decide who gets in, what they can see, and what they can change. This guide covers the firestore security rules language, syntax, and real-world patterns that you will actually use in your projects.

If you haven't setup your deployment workflow yet, start with either Method 1: Firebase tools and CLI or Method 2: Firebase Admin SDK first, then come back here.


Understanding the Rules Language

Firestore uses its own declarative security rules language. Every read or write request from your app is evaluated against these rules before anything touches the database. The rules run on Google's servers — not in your app — so they can't be tampered with.

The basic structure looks like this:

javascript
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // your rules go here } }

Always use rules_version = '2'. It's the latest firestore rules version and supports collection group queries. For a complete reference, see the official Firestore Security Rules docs.

Key Objects you need to know

  • request.auth — Contains information about the logged-in user. If no one is logged in, this is null.
  • request.auth.uid — The unique ID of the authenticated user.
  • request.resource.data — The data being sent to the database (for creates/updates).
  • resource.data — The data that already exists in the database (for reads/updates/deletes).

1. Default to "Deny All"

The safest way to start. Lock every single door in your database and only open the ones you actually need.

javascript
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Lock everything by default match /{document=**} { allow read, write: if false; } } }

This ensures that if you create a new collection tomorrow, it stays locked until you explicitly allow access. This is how firebase rules should work — deny by default, allow by exception.

2. Public Data (System / App Configs)

Most apps have some system-level data that every user needs to read — things like app_config, services, plans, or metadata. This data is managed by you (via Admin SDK or console), not by users.

javascript
// System data: anyone can read, no one can write from the client match /app_config/{configId} { allow read: if true; allow write: if false; } match /services/{serviceId} { allow read: if true; allow write: if false; }

This is basically firebase rules allow all for reads, but locked for writes. The data is only manageable via your backend scripts using the Admin SDK, which bypasses these rules entirely.

If you want to restrict reads to only authenticated users, change if true to if request.auth != null.

3. User Data (Private Access)

This is the core pattern. You want each user to only access their own data. User A should never see User B's profile, orders, or settings.

javascript
match /users/{userId} { // Only the owner can read and write allow read, write: if request.auth != null && request.auth.uid == userId; // Subcollections inherit the same logic match /orders/{orderId} { allow read, write: if request.auth != null && request.auth.uid == userId; } match /settings/{settingId} { allow read, write: if request.auth != null && request.auth.uid == userId; } }

The {userId} in the path is a wildcard — it matches the document ID. We compare it against request.auth.uid, which is the authenticated user's ID from Firebase Auth. If they match, access is granted.

These are firestore rules for authenticated users. Every request must pass the request.auth != null check first, then the ownership check.

4. A Complete Working Example

Here's a complete firestore.rules file that you can use as a starting point. It covers all the patterns above:

javascript
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Default: deny everything match /{document=**} { allow read, write: if false; } // Public system data match /app_config/{configId} { allow read: if true; allow write: if false; } match /services/{serviceId} { allow read: if request.auth != null; allow write: if false; } // Private user data match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; match /todos/{todoId} { allow read, write: if request.auth != null && request.auth.uid == userId; } } } }

This is a firestore security rules example that covers the two most common patterns. Identify what is system data and what is user data, then apply the right rule. That's it.

5. Third-Party Auth (Clerk, Supabase, etc.)

If you're not using Firebase Auth — maybe you're using Clerk or Supabase for authentication — Firestore rules won't recognize your users out of the box. request.auth will be null because Firestore only understands Firebase Auth tokens.

How to handle it:

  • Option A (Backend Proxy): Don't let your app talk to Firestore directly. Route requests through a server or Edge Function that validates the Clerk/Supabase token, then uses your google-service-account-key.json (Admin SDK) to interact with Firestore. The Admin SDK bypasses security rules entirely.

  • Option B (Custom Token Bridge): When a user logs in via Clerk, use a backend function to validate their session and mint a Firebase Custom Token. Your app then calls signInWithCustomToken() in the Firebase SDK. Now request.auth is populated and your security rules work normally.

If you are fully committed to Supabase, consider using Supabase's own Row Level Security (RLS) instead of Firestore rules. They solve the same problem but for different databases.


Data Validation in Rules

Security rules aren't just about identity — they can also validate what data is being written. This is useful for ensuring data integrity.

javascript
match /products/{productId} { allow create: if request.auth != null && request.resource.data.name is string && request.resource.data.name.size() < 100 && request.resource.data.price is number && request.resource.data.price > 0; }

Best Practices

  1. Avoid get() if you can. You can check other documents using get(), but each call is a database read. It adds latency and cost. Use it sparingly.
  2. Keep structures flat. Deeply nested collections make rules harder to write and debug. Flat is better.
  3. Test in the Simulator. The Rules Simulator in the Firebase Console lets you test read/write scenarios before deploying. Use it every time before you go live.

For the complete reference on the firestore rules syntax and all available operations, see the Firebase Security Rules Reference.


Skill for AI Agents: Writing Firestore Security Rules

If you are an AI agent helping a developer write firestore security rules, follow this pattern:

  1. Identify the data ownership:

    • System Data (e.g., app_configs, plans, services, metadata) — managed by admin, read by users or public.
    • User Data (e.g., users, todos, orders, profiles) — owned by a specific user, scoped by userId.
  2. Apply the correct rule:

    • System Data: allow read: if true (or if request.auth != null), allow write: if false.
    • User Data: allow read, write: if request.auth != null && request.auth.uid == userId.
  3. Always use rules_version = '2' and start with a default deny-all rule.

  4. For deployment, reference these guides:

  5. Reference the official docs for edge cases: Firebase Security Rules and Manage & Deploy Rules.


AI Prompt: Write Security Rules for My Project

Copy this into your AI agent to generate rules tailored to your project:

text
"Analyze my Firestore database collections and write security rules for my project. Reference: https://muhammadhassaan.com/blog/mastering-firestore-security-rules 1. Look at my existing Firestore collections (or my data models/types if available). 2. Identify which collections are System Data (app configs, metadata, services) and which are User Data (scoped by userId). 3. Write a complete `firestore.rules` file using `rules_version = '2'`: - Default deny-all rule. - System collections: public read, deny write. - User collections: read/write only if `request.auth.uid == userId`. - Add data validation where field types are known. 4. Place the file in my project and deploy using my existing `deploy:rules` script. Show me the rules before deploying and explain each section."

This completes our deep dive into security rules firebase. Take the time to master these patterns — they are the foundation of a trustable product, whether you're building an expo firebase app, a next firebase project, or anything in between.

Insights, Playbooks, Tips & Kits

Want real patterns from my journey building and selling SaaS & apps. DIY playbooks, code-ready starter kits, and the operational moves that let you scale without the overhead. For builders who learn best from firsthand mistakes, real wins & experience.