Firebase Security Rules Guide

Tuesday, January 21, 2025

Firestore security rules are essential for managing access to your database. They allow you to control who can read or write data while ensuring sensitive information remains protected. This guide provides practical steps and examples to help you create clear and secure rules for your Firestore projects.


Core Concepts

  • Deny All by Default: Always start with a locked database and open up access only where necessary.
  • Rule Evaluation Order: Rules are checked from top to bottom, and the first matching rule applies.
  • Simplify with Functions: Use functions to avoid repeating logic and make your rules easier to maintain.

  • Setting Up Basic Rules

    Lock Everything by Default

    This ensures no data is accessible until explicitly allowed:

    Language: plain text
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /{document=**} {
          allow read, write: if false;
        }
      }
    }

    Limit Access to Specific Actions

    Firestore allows you to target specific actions, such as reading or writing:

  • allow read: Includes document and collection reads.
  • allow get: Applies only to single document reads.
  • allow list: Applies to collection queries.
  • allow create, update, delete: Controls write operations.
  • Example: Allow only read operations for a specific collection:

    Language: plain text
    match /posts/{postId} {
      allow read: if true;
      allow write: if false;
    }

    Understanding Request and Resource Objects

  • request: Contains details about the incoming request, such as user authentication and request data.
  • resource: Represents the current state of the document being accessed.
  • Example: Use a function to check ownership:

    Language: plain text
    function isOwner() {
      return request.auth.uid == resource.data.ownerId;
    }

    User-Based Access Control

    Allow Access for Authenticated Users

    Only signed-in users can read or write:

    Language: plain text
    match /posts/{postId} {
      allow read, write: if request.auth != null;
    }

    Restrict Access to Document Owners

    Grant access to a user based on their uid:

    Language: plain text
    match /accounts/{userId} {
      allow read, write: if request.auth.uid == userId;
    }

    Protect Data in Multi-User Scenarios

    Ensure users can only modify their own data:

    Language: plain text
    match /posts/{postId} {
      allow create: if request.auth.uid == request.resource.data.uid;
      allow update, delete: if request.auth.uid == resource.data.uid;
    }

    Prevent Anonymous Access

    Block access for users signed in anonymously:

    Language: plain text
    match /posts/{postId} {
      allow create: if request.auth.token.firebase.sign_in_provider != 'anonymous';
    }

    Advanced Access Control

    Allow Public Access with Exceptions

    Make all data public except for specific collections:

    Language: plain text
    match /{document=**} {
      allow read, write: if false;
    }
    
    match /{collectionName}/{docId} {
      allow read: if collectionName != 'private-collection';
    }

    Validate Data Before Writing

    Ensure incoming data meets specific conditions:

    Language: plain text
    function isValidEntry() {
      return request.resource.data.amount > 10 &&
             request.resource.data.category in ['js', 'ts', 'rust', 'python'];
    }
    
    match /products/{productId} {
      allow create: if isValidEntry();
    }

    Time-Based Restrictions

    Enforce time-based constraints to throttle updates:

    Language: plain text
    function isThrottled() {
      return request.time < resource.data.lastUpdate + duration.value(1, 'm');
    }
    
    match /posts/{postId} {
      allow update: if !isThrottled();
    }

    Reusable Functions

    Reusable functions make your rules more maintainable. For example:

    Language: plain text
    service cloud.firestore {
      match /databases/{database}/documents {
        function isSignedIn() {
          return request.auth != null;
        }
    
        function isOwner(userId) {
          return request.auth.uid == userId;
        }
    
        function hasVerifiedEmail() {
          return request.auth.token.email_verified;
        }
    
        match /orders/{orderId} {
          allow create: if isSignedIn() && hasVerifiedEmail() && isOwner(request.resource.data.userId);
          allow read, update, delete: if isSignedIn() && isOwner(resource.data.userId);
        }
      }
    }
    

    Testing Your Rules

  • Firestore Rules Playground: Test your rules directly in the Firebase Console.
  • Firebase Emulator Suite: Use the emulator to locally simulate your app’s requests and verify your rules.

  • Conclusion

    Effective security rules are the foundation of any Firestore project. By starting with strict access, validating data, and using reusable functions, you can create a secure and scalable database. Regularly test your rules to ensure they meet your app’s requirements.