DEV Community

Cover image for Exploring Firebase Database Security Rules
Giorgio Boa for This is Learning

Posted on

4 1

Exploring Firebase Database Security Rules

Firebase Realtime Database offers incredible power and ease-of-use for building collaborative, real-time applications.
However, its public-facing nature means security is not just an option—it's a fundamental requirement.

Misconfigured security rules can leave your data vulnerable to unauthorized access, modification, or deletion.

Security Rules

Security Rules are server-side configurations that define who can access your database data, how they can access it (read/write), how your data should be structured, and which fields should be indexed for querying. They are written in a JavaScript-like expression language within a JSON structure.

These rules are the primary mechanism for protecting your data in Firebase Realtime Database.

Since Firebase apps often involve more client-side logic and direct database access from the client, robust server-enforced rules are essential. Without proper rules, anyone guessing your project ID could potentially read, modify, or delete your entire database.

By default, new databases start in "locked mode" (.read: false, .write: false) or "test mode" (allowing access for a limited time) to prevent accidental exposure.

default rules

Every single read and write operation attempted against your RTDB is evaluated against your security rules before any data access occurs. These rules live and are enforced on Firebase servers, ensuring that even a compromised or malicious client cannot bypass them. If the rules allow the operation for the specific path requested, the operation proceeds. If not, it's rejected, and the client receives a "permission denied" error.

Manage rules

You can view and edit your rules directly in the Firebase Console.
Go to your Firebase project and navigate to "Realtime Database" in the left-hand menu. Select the "Rules" tab.

Rules tab

Security Rules Syntax

Security rules are defined within a single JSON object, where the keys represent paths in your database, and the values are rule definitions for those paths.

{
  "rules": {
    // Rules go here, matching your database structure
    "users": {
      // Rules for the /users path
    },
    "posts": {
      // Rules for the /posts path
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Security Rule Types

The following keys define behavior at a specific path:

.read

A boolean expression (evaluating to true or false). If true, read operations (like get, onValue) are allowed at this path and potentially deeper paths.

.write

A boolean expression. If true, write operations (like set, update, remove) are allowed at this path and potentially deeper paths.

.validate

A boolean expression used to enforce data structure and format before a write occurs. It uses the newData variable to inspect the data being written.

A write must pass validation at the specific path being written and all parent paths that have .validate rules.

.indexOn ( crucial for performance )

You can specify which child keys should be indexed to optimize querying (orderByChild, orderByValue), the value is a string or an array of strings.

Built-in Variables

Rules provide predefined variables to make context-aware decisions:

auth contains information about the authenticated user making the request (or null if unauthenticated). Includes auth.uid and auth.token (payload of the user's Firebase Auth ID token, including custom claims).

root is the rule data snapshot representing the root of your database before the attempted operation. Used to access data anywhere in the database. Use with caution, as cross-path reads can impact performance.

data is the rule data snapshot representing the data at the current path before the attempted operation. Used for validation based on existing state.

newData is the rule data snapshot representing the data at the current path as it would exist after the attempted write operation. Used primarily in .write and .validate rules to check the incoming data.

now is the current server timestamp in milliseconds since the Unix epoch. Useful for setting creation/modification timestamps and validation.

$variable is a wildcard variables capture path segments dynamically. (e.g., in users/$userId, $userId captures the specific user ID).

Basic Conditions

Let's start with a basic configuration.

{
  "rules": {
    "public_data": {
      ".read": true,  // Anyone can read
      ".write": false // No one can write
    },  
    "admin_only_data": {
      ".read": "auth != null && root.child('admins').child(auth.uid).exists()", // Only admins can read
      ".write": "auth != null && root.child('admins').child(auth.uid).exists()" // Only admins can write
      // auth != null
      // This part of the rule ensures that the user attempting the read or write operation is actually 
      // logged in to your Firebase application.

      // root.child('admins').child(auth.uid).exists()
      // This part checks if the currently logged-in user's UID is listed as a key directly under the 
      // /admins node in your database. This is a common pattern for managing a list of administrator users.
    },
    "admins": {
      // '.exists()' only checks for the key's presence.
      "admin_user_uid": {},
      "another_admin_uid": {}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Cascade Effect

.read and .write rules cascade downwards. If a rule grants access at a higher level in the database path, it cannot be revoked by a more restrictive rule at a deeper level for that same operation type (.read or .write).

{
  "rules": {
    "users": {
      ".read": true, // Grants read access to ALL data under /users
      "$userId": {
        "private_info": {
          // This rule is IGNORED for reads because the parent granted read access
          ".read": false
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Because .read: true is set at /users, all data under /users, including /users/$userId/private_info, becomes readable by anyone, regardless of the deeper rule.

This is a critical concept and a common source of security flaws. Always apply the principle of least privilege: grant access as deep in the structure as possible.

Identifying Users

The most common pattern is restricting access based on the user's unique ID (auth.uid). This is essential for user-owned data.

{
  "rules": {
    "users": {
      // Use a wildcard to match any user ID
      "$userId": {
        // Only the user whose UID matches the path segment can read/write
        ".read": "auth != null && auth.uid === $userId",
        ".write": "auth != null && auth.uid === $userId"
        // Note: You might want broader read access, e.g., allow any authenticated user to read profiles
        // ".read": "auth != null",
        // ".write": "auth != null && auth.uid === $userId"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Leveraging Authentication Provider Data

The auth.token object contains the decoded Firebase ID token payload. This includes standard claims (like email, email_verified) and any custom claims you've set.

Custom claims are powerful for implementing role-based access control (RBAC).

{
  // Example using a custom claim 'isAdmin'
  "rules": {
    "admin_reports": {
      ".read": "auth != null && auth.token.isAdmin === true",
      ".write": "auth != null && auth.token.isAdmin === true"
    },
    "user_settings": {
      "$userId": {
        // User can write their settings, admin can also write
        ".write": "auth != null && (auth.uid === $userId || auth.token.isAdmin === true)"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Accessing Existing Data

The data variable refers to the data at the current rule path before the operation. It's useful for conditional writes or validation based on the current state.

{
  "rules": {
    "items": {
      "$itemId": {
        // Allow update only if the item is not locked
        ".write": "auth != null && data.child('isLocked').val() !== true"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Wildcards ($variable) for Dynamic Paths

Wildcard variables, prefixed with $, capture dynamic segments of a path. These captured values can then be used within rule expressions.

{
  "rules": {
    "posts": {
      "$postId": { // $postId captures the unique ID of the post
        ".read": "auth != null", // Authenticated users can read any post
        // Allow write only if the post's authorId matches the authenticated user's UID
        ".write": "auth != null && (newData.child('authorId').val() === auth.uid || data.child('authorId').val() === auth.uid)",
        // Ensure authorId exists and matches user on creation, and cannot be changed on update
        ".validate": "newData.hasChildren(['title', 'content', 'authorId']) && newData.child('title').isString() && newData.child('title').val().length > 5 && newData.child('authorId').val() === auth.uid && (!data.exists() || newData.child('authorId').val() === data.child('authorId').val())"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Accessing Data Elsewhere

The root variable allows you to read data from anywhere in the database as it exists before the operation. This is powerful but use it judiciously, as it can impact performance and complexity. It's often used for checking roles or related data.

{
  "rules": {
    // Assume roles are stored at /user_roles/$userId/role
    "sensitive_data": {
      "$dataId": {
        ".read": "auth != null && root.child('user_roles').child(auth.uid).child('role').val() === 'admin'",
        ".write": "auth != null && root.child('user_roles').child(auth.uid).child('role').val() === 'admin'"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Improve Query Performance

When you run a query like orderByChild, Firebase can use indexes to quickly find the matching items without downloading the entire list. This dramatically improves query performance.

{
  "rules": {
    "items": {
      ".read": true,
      ".write": true,
      // Define an index on the 'category' child key for all children under 'items'.
      // This optimizes queries that filter or order by 'category'.
      ".indexOn": "category"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Remember to index the fields you frequently use for filtering (equalTo, startAt, endAt) or sorting (orderByChild). You can also specify multiple fields to index using an array:
.indexOn: ["category", "name"].

Test Firebase Rules With Simulator

The Firebase Console provides a "Rules Playground" (accessible from the Rules tab). It allows you to simulate read, write, or delete operations.

The simulator will tell you if the operation was allowed or denied based on your currently edited (but not yet published) rules and show which rule lines were evaluated. It's great for quick checks and understanding rule evaluation flow.

Best Practices for Writing Secure and Maintainable Rules

Start Secure and begin with locked-down rules (.read: false, .write: false) and grant access incrementally as needed.

Grant only the minimum necessary permissions at the deepest possible path in your data structure.

Leverage Firebase Authentication (auth variable) extensively. Avoid rules that allow writes without authentication (auth == null) unless strictly necessary for a specific public feature.

Use .validate rules rigorously to enforce data types, formats, required fields, ranges, and consistency. Don't trust client-side validation alone.

Separate public and private user data. Ensure only authorized users (usually the owner or admins) can access sensitive fields.

Use .indexOn to ensure your queries remain performant as your dataset grows. Define indexes early.

Add comments (// for single line, though not officially supported everywhere, often works in editors; consider structuring rules clearly).


Firebase Realtime Database security rules provide a powerful, server-enforced mechanism for controlling data access and structure. From basic read/write permissions based on authentication (auth) to complex conditional logic using existing (data) and incoming (newData) data, rules offer granular control. Understanding concepts like the cascade effect, utilizing wildcards ($variable), enforcing structure with .validate, and optimizing queries with .indexOn are key skills. By embracing best practices and leveraging the testing tools available, developers can confidently build secure, scalable, and performant applications on Firebase.


You can follow me on GitHub, where I'm creating cool projects.

I hope you enjoyed this article, don't forget to give ❤️.
Bye 👋

Top comments (0)