Firestore Security Rules examples

May 24, 2020 (updated June 21, 2020)

Firestore Security Rules allow you to restrict access to your Firestore database and perform data validation on writes and reads. If you allow your app to perform read and write operations client-side (which is the case with most Firebase apps), there is nothing that stops people from writing their own code (or tampering with yours) to access your Firestore database.

You should create your security rules as if someone else were using the Firebase SDKs with your keys.

This is a collection of real-world use cases and how they can be implemented using Firestore Security Rules.

Please refer to the official Firestore docs for a complete reference on how security rules are written and structured.

Operations overview

  • get: Get a document
  • list: List collections in a document
  • create: Create a document
  • update: Update a document
  • delete: Delete a document
  • read: Same as get and list
  • write: Same as create, update and delete

Usage

match /<some_path>/ {
  allow create: if <some_condition>;
  allow read, update: if <some_condition>;
  allow delete: if <some_condition>;
}

Allow all (public access)

This rule will allow everyone to read and write to the path (create and read a document in the dogs collection).

match /dogs/{dogs} {
  allow read, write: if true;
}

Allow no one (completely private)

No one will be able to read or write to the path. Note that collections are closed for reads and writes by default.

match /dogs/{dog} {
  allow read, write: if false;
}

Allow authenticated users

Only authenticated users will be able to write and read documents in the dogs collection.

match /dogs/{dog} {
  allow read, write: if request.auth != null;
}

Users can only read and write their own data

Only authenticated users are allowed to create a document in the users collection. Reading, updating and deleting is restricted to the user who owns the document:

match /users/{userId} {
  allow create: if request.auth != null;
  allow read, update, delete: if request.auth.uid == userId;
}

Only the document owner is allowed to write and read documents in the dogs collection. This also restricts users from creating a dog under another user's ID:

match /users/{userId}/dogs/{dog} {
  allow read, write: if request.auth.uid == userId;
}

Instead of storing the user's ID in the document path, you can store it on the document itself. In the following example, the document contains the owner's user ID (resource.data.uid), and only the owner is allowed to read it. To create a document, the new resource must contain a uid that matches the authenticated user's:

match /dogs/{dog} {
  allow read: if request.auth.uid == resource.data.uid;
  allow create: if request.auth.uid == request.resource.data.uid;
}

Allow multiple users (an arbitrary group of users)

The document contains an array of user IDs (resource.data.contributors). Only users who have their ID listed in this array are allowed to read the path.

match /dogs/{dog} {
  allow read: if request.auth.uid in resource.data.contributors;
}

Role-base access (ACL)

Instead of adding the user ID to the document itself, you can query another Firestore document to see if the user has the required role. In this example, users can only delete a document in the articles collection if they have the "admin" role (this role is "global", not per-document). Furthermore, users can only comment on an article if they're not blacklisted, i.e. if their user ID doesn't exist in the blacklist collection.

match /articles/{articleId} {
  allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "admin";

  match /comments/{commentId} {
    allow create: if exists(/databases/$(database)/documents/blacklist/$(request.auth.uid));
  }
}

Alternatively, each document can specify a list of users and their roles. This security rule checks if the authenticated user has the required role to create the specific document.

match /articles/{articleId} {
  allow create: if request.resource.data.roles[request.auth.uid] == 'owner';
}

To use the above security rule, the document must contain a map of users and roles, like this:

{
  title: "Hello, world!",
  roles: {
    uid1: "owner",
    uid2: "reader",
    uid3: "reader",
    // ...
  }
}

Require a verified email address

Users must have a verified email address to read or write to the dogs collection.

match /dogs/{dog} {
  allow write, read: if request.auth.token.email_verified;
}

Require a specific sign-in provider

Only users who have signed in with a phone number can read the document:

match /dogs/{dog} {
   allow read: if request.auth.token.firebase.sign_in_provider == "phone";
}

Refer to the available sign-in providers for more options.

Only allow specific fields

The document can only contain the fields name and description. If a user tries to provide other fields when creating a document, the write is denied.

match /dogs/{dog} {
  allow create: if request.resource.data.keys().hasOnly(["name", "description"]);
}

Note that request.resource.data represents the document after the write operation has succeeded (i.e. the "future" document). So if this were an update operation, and if the existing document contained another field like createdAt, the above rule would fail because request.resource.data would contain name, description and createdAt. See below for how to deal with updates.

Only allow updating specific fields

Only allow updating the name and description field.

match /dogs/{dog} {
  allow update: if request.resource.data.diff(resource.data).affectedKeys().hasOnly(["name", "description"]);
}

In other words: If we compare the new resource (request.resource.data) with the existing resource (resource.data), only name and description are allowed to be different (i.e. changed).

Deny updating a field

As an alternative to the above, this rule will deny writes if a specific filed (isPromoted) field is changed.

match /dogs/{dog} {
  allow update: if request.resource.data.isPromoted == resource.data.isPromoted;
}

Even though we check if the incoming data is equal to the existing data, the client doesn't need to actually send the isPromoted field; remember that request.resource.data represents the resource after a successful write operation (the "future" document), so if isPromoted is present on the existing document, it will be present in request.resource.data as well.

But what if isPromoted isn't present on the existing document? Perhaps a Firebase Function is supposed to populate it later. If this is the case, the security rule's reference to request.resource.data.isPromoted will trigger an error ("Property name is undefined on object"). This will deny the operation, so no harm is done, but if you want to avoid the error you can modify the rule and prepare for the missing field.

Here's a custom function that takes this into consideration:

function fieldNotUpdated(field) {
  return !(field in request.resource.data)
         || (
           (field in resource.data)
           && request.resource.data[field] == resource.data[field]
         );
}

// Usage:
match /dogs/{dog} {
  allow update: fieldNotUpdated("isPromoted");
}
  • If neither the request nor the existing resource contains the field, the update is allowed.
  • If the existing resource contains the field, but the user doesn't supply it, the update is allowed.
  • If the existing resource does not contain the field, and the user supplies it, the update is denied.
  • If the existing resource contains the field, and the user supplies it with the same value, the update is allowed.
  • If the existing resource contains the field, and the user supplies it with another value, the update is denied.

Require document fields

Require the fields name and description.

match /dogs/{dog} {
  allow write: if request.resource.data.keys().hasAll(["name", "description"]);
}

Validate data types (string, timestamp, number ...)

Make sure the user sends the correct data types.

match /dogs/{dog} {
  allow write: if request.resource.data.name is string
               && request.resource.data.age is number
               && request.resource.data.createdAt is timestamp
               && request.resource.data.isHungry is boolean;
}

Timestamp equals current time (server timestamp)

Check if the field createdAt is a server timestamp; if your client app sends the createdAt field, make sure it was created using firebase.firestore.FieldValue.serverTimestamp().

match /dogs/{dog} {
  allow write: if request.resource.data.createdAt == request.time;
}

Maximum/minumum number of characters

The dog's name can be no longer than 50 characters.

match /dogs/{dog} {
  allow write: if request.resource.data.name is string
               && request.resource.data.name.size() < 50
}

Check if another document exists

Only allow a document to be created if another document exists (in this case, a document in the users collection with an ID equal to the authenticated user's).

match /dogs/{dog} {
  allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid));
}

Check if another document has a value

Users can only create a document in the articles collection if they have a user document with the admin field set to true.

match /articles/{articleId} {
  allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
}

Custom functions

You can organize rules and logic in custom functions. This prevents you from having to duplicate knowledge, and helps you create a more readable set of rules.

While not production ready, the following example shows some possibilities:

function isSignedIn() {
  return request.auth != null;
}

function isResourceOwner(rsc) {
  // Check if a resource is owned by the authenticated user.
  return isSignedIn() && getOwner(rsc) == request.auth.uid;
}

function getOwner(rsc) {
  // Get the owner (user ID) of the resource
  return rsc.data.owner;
}

function isAdmin() {
  return get(/databases/$(database)/documents/user/$(request.auth.uid)).data.isAdmin == true;
}

function isFutureDate(date) {
  return date is timestamp && date > request.time;
}

function incomingData() {
  return request.resource.data;
}

function existingData() {
  return resource.data;
}

// Example usage:

match /articles/{articleId} {
   allow update: if isResourceOwner(resource)
                 && incomingData().keys().hasOnly(["title", "body"]);

   allow delete: if isAdmin();

   match /comments/{commentId} {
     // Allow adding a comment if the user owns the parent article
     allow create: if isResourceOwner(get(/databases/$(database)/documents/articles/$(articleId)))
   }
}

Notes

  • The Admin SDK bypasses Firestore security rules. If you have complex requirements, consider doing validations in a Cloud Function.
  • You can create automatic test for your security rules.

Closing remarks

I'll try to keep these examples updated, but please give a heads-up in the comments if you find a mistake!