How to Write Firebase Security Rules

Share
How-To Guide

How to Write Firebase Security Rules

Protect your Firestore and Realtime Database

TL;DR

TL;DR: Use request.auth.uid to verify the user owns the data. Never use allow read, write: if true in production. Test rules in the Firebase console before deploying. Firestore rules are different from Realtime Database rules.

Default Rules Are Dangerous

Firebase projects often start with "test mode" rules that allow anyone to read and write all data. These rules expire after 30 days, but many developers forget to update them. Always check your rules before launching.

Firestore Security Rules

Basic Structure

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

Pattern 1: User-Owned Documents

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Users can only access their own documents
    match /users/{userId} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }

    // Todos owned by users
    match /todos/{todoId} {
      allow read: if request.auth != null
                  && resource.data.userId == request.auth.uid;

      allow create: if request.auth != null
                    && request.resource.data.userId == request.auth.uid;

      allow update, delete: if request.auth != null
                            && resource.data.userId == request.auth.uid;
    }
  }
}

Pattern 2: Public Read, Authenticated Write

match /posts/{postId} {
  // Anyone can read published posts
  allow read: if resource.data.status == 'published';

  // Only authenticated users can create posts
  allow create: if request.auth != null
                && request.resource.data.authorId == request.auth.uid;

  // Only the author can update/delete
  allow update, delete: if request.auth != null
                        && resource.data.authorId == request.auth.uid;
}

Pattern 3: Data Validation

match /products/{productId} {
  allow create: if request.auth != null
                // Required fields
                && request.resource.data.keys().hasAll(['name', 'price', 'createdAt'])
                // Type validation
                && request.resource.data.name is string
                && request.resource.data.price is number
                && request.resource.data.price > 0
                // Timestamp validation
                && request.resource.data.createdAt == request.time;
}

Pattern 4: Role-Based Access

// Assumes users have a 'role' field in their profile
match /admin/{document=**} {
  allow read, write: if request.auth != null
                     && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}

// Or use custom claims (set via Admin SDK)
match /admin/{document=**} {
  allow read, write: if request.auth != null
                     && request.auth.token.admin == true;
}

Realtime Database Rules

Realtime Database uses a different JSON-based syntax:

Basic Structure

{
  "rules": {
    ".read": false,
    ".write": false,
    // Specific rules override these defaults
  }
}

User-Owned Data

{
  "rules": {
    "users": {
      "$userId": {
        ".read": "$userId === auth.uid",
        ".write": "$userId === auth.uid"
      }
    },
    "todos": {
      "$todoId": {
        ".read": "data.child('userId').val() === auth.uid",
        ".write": "(!data.exists() && newData.child('userId').val() === auth.uid) ||
                   (data.child('userId').val() === auth.uid)"
      }
    }
  }
}

Testing Your Rules

Firebase Console

  1. Go to Firebase Console → Firestore → Rules
  2. Click "Rules Playground"
  3. Select operation type (get, list, create, update, delete)
  4. Enter document path and optional auth context
  5. Run the simulation

Emulator Testing

// Install Firebase emulators
firebase init emulators

// Start emulators
firebase emulators:start

// In your test file
import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing';

describe('Firestore rules', () => {
  it('allows users to read their own data', async () => {
    const db = getTestFirestore({ uid: 'user123' });
    await assertSucceeds(db.collection('users').doc('user123').get());
  });

  it('denies users from reading others data', async () => {
    const db = getTestFirestore({ uid: 'user123' });
    await assertFails(db.collection('users').doc('other-user').get());
  });
});

Common Mistakes

Never Do This in Production

// DANGEROUS: Anyone can read/write everything
match /{document=**} {
  allow read, write: if true;
}

// DANGEROUS: Allows all authenticated users full access
match /{document=**} {
  allow read, write: if request.auth != null;
}
MistakeProblemFix
if trueAnyone can accessAlways check auth and ownership
if request.auth != nullAny logged-in user can accessVerify user owns the data
No validation on createUsers can set any userIdValidate request.resource.data.userId
Recursive wildcards{document=**} is too broadBe specific about paths

What's the difference between resource and request.resource?

resource.data contains the existing document data. request.resource.data contains the data being written. Use resource for read/delete operations and request.resource for create/update validation.

Why are my rules not working?

Common issues: 1) Rules haven't been deployed yet, 2) You're using the Admin SDK which bypasses rules, 3) Path matching is incorrect, 4) Auth context isn't being passed correctly.

How do I debug rules?

Use the Rules Playground in Firebase Console, check the Firebase logs in Google Cloud Console, or use the Firebase emulator with debug logging enabled.

Related guides:Firebase Security Guide · Firebase Auth Rules · Supabase vs Firebase Security

How-To Guides

How to Write Firebase Security Rules