Cloud Firestore improved on many things over its predecessor, Realtime Database. One of which is security. Cloud Firestore’s security rules are more flexible and easier to write than those of the Realtime Database, especially without the trouble of cascading rules.

Read on and I’ll let you decide for yourself which set of rules you find easier to write and manage.

In my last post about Why Every Developer Needs a Blog, I touched upon Soft Skills. I still can’t recommend it enough. I’m almost at the end of the book and it’s already changed my life. It’s a software developer’s life manual split into sections of Career, Marketing, Productivity, Learning, Finance, Fitness, and Spirit.

The Basics: Match and Allow

service cloud.firestore {
  match /databases/{database}/documents {

    // Match any document in the 'cities' collection
    match /cities/{city} {
      allow read: if <condition>;
      allow write: if <condition>;
    }
  }
}

a basic example of security rules

Match points to a document you want to write your security rules for. Note that match only works with documents, not collections. There is a way to point to all documents in a collection with wildcards, but more on that later.

Allow sets the actual rule. You should always include a condition with allow statements, even if it’s just a simple if true.

Multiple Match Statements

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any documetnt in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

It’s possible for a document to have more than one match statement. In this case, access will be allowed if any of the allow conditions are true. In the example above, all read and writes to the /cities/{city} path are allowed because the second statement returns true, even though the first statement returns false.

Different Allow Operations

service cloud.firestore {
  match /databases/{database}/documents {
    // A read rule can be divided into get and list rules
    match /cities/{city} {
      // Applies to single document read requests
      allow get: if <condition>;

      // Applies to queries and collection read requests
      allow list: if <condition>;
    }

    // A write rule can be divided into create, update, and delete rules
    match /cities/{city} {
      // Applies to writes to nonexistent documents
      allow create: if <condition>;

      // Applies to writes to existing documents
      allow update: if <condition>;

      // Applies to delete operations
      allow delete: if <condition>;
    }
  }
}

With Firestore’s Security Rules, we have more flexibility than just read and write.

Read can be split into get and listGet refers to operations of retrieving a single document, while list refers to operations that retrieve groups of documents by querying their collections.

Write can be split into createupdate, and delete. I don’t think I need to explain each of them.

More Complex Conditions

We won’t only be writing true and false all day. We have the variables request and resource, and functions exists and get.

Request

Field Description
 auth Refers to the authenticated user and any corresponding data
 path Refers to the path that a https://firebase.google.com/docs/firestore/reference/security/authrequest is being performed against.
 query Contains the static properties of a query being requested.
 resource Contains data and metadata about the document being written.
 time Contains a timestamp representing the current server time a request is being evaluated at.
 writeFields Contains a list of the field paths that the current request is updating.

To make it a little clearer, here’s an example.

// Allow requests from authenticated users
allow read, write: if request.auth != null;

// Allow a read if the file was created less than one hour ago
allow read: if request.time < resource.data.timeCreated + duration.value(1, 'h');

Resource

Resource refers to a document. It contains a map of the values stored in the document in resource.data.

It also contains metadata such as the document name and id, often prefixed with __, as with __name__ and __id__.

// Allow a read if the document being read contains certain fields
allow read: if resource.data.keys().hasAll(['name', 'age'])
            && resource.data.size() == 2
            && resource.data.name is string
            && resource.data.age is int
            && resource.__name__ is path;  // projects/projectId/databases/(default)/documents/...

Note that this is different from request.resource in that this resource contains the data for the document at the requested path while request.resource contains the data of the document being written.

Exists

match /rooms/{roomId} {
      match /messages/{messageId} {
        allow read: if exists(/databases/$(database)/documents/rooms/$(roomId)/users/$(request.auth.uid));
      }
}

Takes in a path and returns true if a document exists at the specified path. Tha path provided must always begin with /databases/$(database)/documents.

Get

match /rooms/{roomId} {
      match /messages/{messageId} {
        allow update, delete: if get(/databases/$(database)/documents/rooms/$(roomId)/users/$(request.auth.uid)).data.isAdmin == true;
      }
}

Takes in a path and returns the document at the specified path. Again, the path provided must always begin with /databases/$(database)/documents.

Hierarchical Data and Wildcards

A security rule will only apply to the matched path. These rules won’t cascade to paths above or below the matched path. You have to explicitly define rules for any subcollections. In this case, say we want to define a set of rules for paths cities/{city} and cities/{city}/landmarks/{landmark}.

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

        // Explicitly define rules for the 'landmarks' subcollection
        match /landmarks/{landmark} {
          allow read, write: if <condition>;
        }
    }
  }
}

Note that here, we’re nesting match statements. The path of the inner match statement is relative to the path of the outer match statement.

Recursive Wildcard Syntax: Matching all paths below

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Of course, if we were to have many different subcollections that extends deep into the hierarchy, explicitly defining rules for each one of them can be very tedious. To match all the documents below a given path, use the wildcard syntax {name=**}.

What about Indexes?

If you know anything about the Realtime Database’s Security Rules, you’ll know that it has one “security rule” that doesn’t have anything to do with security: indexOn. We can still manage indexes with Firestore, but it’s now separate from the security rules.

Every Firestore document is indexed by default. If you want to make compound or sorted queries, simply go to the Indexes tab next to the Rules tab in the console. From there, making the indexes is pretty self-explanatory.

Conclusion

I think that’s about it for Cloud Firestore’s security rules. At the very least, it should be enough so that you can navigate through the official docs for it on your own if you need to get into the finer details.

A couple of you from Reddit have asked me to write about this after I wrote Understanding Firebase Realtime Database Security Rules. Although I already had this post planned, I’m glad to have gotten input from you! Feel free to leave any suggestions in the comments if you want me to touch on a particular topic.

Newsletter

Subscribe to the Newsletter