
I’ve been there… You’re staring at a blank firestore.rules page, not knowing where to start. You want to make sure that your website is secure, but you’re not sure what to do and you are worried that you will do it incorrectly. But have no fear! These Firestore rules examples will give you the base that you need to safely secure your website or application.
Firestore rules are actually fairly simple and follow a logical structure. The examples below go over common situations that might arise in your app, and how to write rules to make sure that your data is safe! Learn these patterns and you will be able to whip up a secured database in no time at all.
Firestore Rules Examples
- Denying All Reads
- Denying All Writes
- Checking if a User is Authenticated
- Checking if a Document Being Accessed Belongs to the Requesting User
- Checking if a Document Being Created Belongs to the Requesting User
- Using Functions
- Verifying a Value’s Type
- Verifying That a Value Belongs to a List of Values
- Verifying a Values Length
- Getting a Document From Another Collection
Denying All Reads
Sometimes you want to store data, but you just don’t want it to be accessed.
Maybe you have sensitive data that you want to persist, but that you don’t want to be accessible via your API. Maybe you didn’t follow the advice in this article and user data is leaking out into places it shouldn’t be and you need to turn off the tap quickly.
Either way there are many legitimate reasons for denying all reads to your database, and luckily, with Firestore rules it is very easy to do.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if false;
}
}
}
Denying All Writes
Denying all writes is another thing that you might find yourself wanting to do. Maybe you have written a bunch of articles and manually added them to your Firestore database. You want to display them on your website, but want to make sure that no one can modify or delete them.
Similarly to the above denying all writes is trivial.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow write: if false;
}
}
}
If you want deny both reading and writing to the database, then the two can be combined like so.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
Checking if a User is Authenticated
If you don’t want to deny all read and write access to your database, and want users to be able to see, create and change things on your website or app, then you are probably going to want them to be authenticated.
The following Firestore rule example does this by checking that the request being made to your database contains a uid.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: request.auth.uid != null;
}
}
}
Checking if a Document Being Accessed Belongs to the Requesting User
Now, using the previous example we made sure that only authenticated users can access our data, but what about if we want to take this one step further?
Often we don’t want to just let all users access all data, but further separate it out, and only let users see their own documents.
To do this in Firestore, when creating documents you should create a userID
field and store the creating user’s uid in it. Then, when trying to access that document later on, check to see if the uid in the request matches the user ID that is stored in the resource being accessed.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /petowners/{ownerId} {
request.auth.uid == resource.data.userId;
}
}
}
Checking if a Document Being Created Belongs to the Requesting User
In addition to only letting users see their own data, we only want to let them write data that belongs to their account. Letting users write to other peoples accounts could get us in all sorts of trouble!
This is done in a similar fashion to the example above. The only difference is that instead of checking the resource
object’s user id, we check the request.resource
object’s one.
The request.resource
object is the document that is being sent in the request to your database.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /petowners/{ownerId} {
request.auth.uid == request.resource.data.userId;
}
}
}
Using Functions
You often want to check the same things over and over again. However, writing the rules out every time is a bad idea.
It takes more time than it should, creates superfluous code and is inconvenient, but worst of all, it is a security hazard.
Let’s say that you have a rule that checks whether a user has permission to access a document or not. It is needed in many different places, for read, delete, modify and create rules. You then copy and paste this rule into each place where it is needed.
It works. Fantastic. Right?
No.
Imagine that now, the requirements for accessing a document change and you go through each instance of the rule you copy and pasted and update them. Cumbersome, but you can deal with it.
However, you missed one instance and a user who shouldn’t have access to a document manages to permanently delete it.
Oops.
This is why you should be using functions and keeping your firestore.rules file DRY.
rules_version = '2';
service cloud.firestore {
function userIsAuthenticated() {
return request.auth.uid != null;
}
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if userIsAuthenticated();
}
}
}
As you can probably see, another advantage to functions is that rules become more readable. This is especially important as rule sets get larger and more complicated.
Out of all of these firestore rules examples, this one is the one that you should remember and put into practice!
Verifying a Value’s Type
One of the great things about Firestore rules is that you can deny the creation or modification of a document, if the value being provided in the request isn’t what you expect it to be.
One way to check for this, is to check for the type of the value. This means that you can check that names are strings, ages are numbers and choices are booleans, for example.
If someone tries to write a value whose type does not match the defined one, it will be denied. Useful for keeping data as clean as possible.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /petowners/{ownerId} {
allow write: if request.resource.data.name is string &&
request.resource.data.email is string;
}
}
}
Verifying That a Value Belongs to a List of Values
Another way to check if a value is what you expect, is to check if the value being provided is in a list of values that you have defined.
For example, if you wanted to store the state of email sending, you could define four possible states; ‘sending’, ‘hard-bounced’, ‘soft-bounced’ and ‘received’.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /emails/{emailId} {
allow write: if request.resource.data.status in ['sending', 'soft-bounce', 'hard-bounce', 'received']
}
}
}
Verifying a Value’s Length
Did you know that RFC 2821 defines the maximum length of an email address in MAIL and RCPT commands to be 254 characters?
No?
Well now you do, and you had better enforce this in your database too!
Luckily in situations like this where you want to limit the length of a value, Firestore has your back once again.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /emailaddresses/{emailAddressId} {
allow write: if request.resource.data.address.size() < 254;
}
}
}
Getting a Document From Another Collection
One of the convenient features of relational databases is the existence foreign keys. Being able to link objects together can be very useful at times.
Due to Firestore being a schema-less, document-based database, we don’t have foreign keys in our toolbelt. We can however store IDs of other documents in a field, and then use that to retrieve them in our rules – using get.
For example, below we have two collections; petowners
and pets
. We want pet owners to only be able to see their own pets.
So, when a request is made to see a pet, we get the owner of it and check that the petowners
document belongs to the currently authenticated user.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /pets/{petId} {
allow read: if get(/databases/$(database)/documents/petowners/$(request.resource.data.ownerId)).data.userId == request.auth.uid
}
}
}
Firestore is a great database to quickly get your apps up and running. Perfect for churning out all the website and app ideas that you have (if you struggle to come up with ideas, check this post out).
But, even for small side projects, if there is ever the chance that you will get users (yourself included!), then considering security from the outset is a must.
Hopefully these Firestore rules examples have shed a bit of light on how you can secure your database too.
‘Ello, I’m Jamal – a Tokyo-based, indie-hacking, FinTech software developer with a dependence on data.
I write Shakespeare-grade code, nowadays mostly in Python and JavaScript and productivity runs in my veins.
I’m friendly, so feel free to say hello!