I want you to learn “how to write Firebase Rules” rather than what specific rules you need to write. Once you understand what kinds of operations are available and how to write clean code, you should be able to write reusable functions for any Firebase app.
TL;DR#
Using Firestore Rules can be cumbersome. However, simplifying your rules into reusable functions can make writing complex, secure apps much easier. Remember the Security Rules Limitations, when to use Trigger Functions instead, and how you are charged per document read.
Firestore Rules#
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// MATCHES
match /users/{document} {
allow read, write: if false;
}
}
}
- Use version 2 by default
- Lock your data by default using
if false
Permission#
allow read
- single and multiple documentsallow get
- a single document withgetDoc
oronSnapshot
allow list
- multiple documents withgetDocs
oronSnapshot
allow write
- create, update, deleteallow create
- create a document withaddDoc
orsetDoc
allow update
- update a document withupdateDoc
orsetDoc
usingmerge
allow delete
- delete a document withdeleteDoc
Resource#
resource
- document being read or writtenresource.data
- map of document dataresource.id
- resource document id as stringresource['__name__']
orrequest.path
- the document name as path
Request#
request
- the incoming request contextrequest.auth
- request authentication contextuid
- user IDtoken
- token map
request.method
- request methodget
list
create
update
delete
request.path
- path of affected resourcerequest.query
- map of query properties when presentlimit
offset
orderBy
request.time
- time request was receivedrequest.resource
- new resource value, present on write requests only
Request Resource#
request.resource
- resource document AFTER writerequest.resource
- map of document dataresource.id
- resource document id as stringresource['__name__']
orrequest.path
- the document name as path
Request token#
request.auth.token
email
email_verified
phone_number
name
sub
firebase.identities
firebase.sign_in_provider
firebase.tenant
Request Patterns#
request.path[3]
- request collectionrequest.path[4]
- request document id orrequest.resource.id
request.path[N]
- request sub-collection and sub-collection document ID
There is currently no way to check if a request is for a collection
or sub-collection
document inside a function. See StackOverflow.
Counters and Timestamp Rules#
- Batch Increment Counters
You can verify increments with rules. - Dates and Timestamps
You can userequest.time
to verifyserverTimestamp()
Ownership#
- Create
- Check for ownership after creation
- Delete
- Check for ownership before deletion
- Update
- Check for ownership both before and after updating
// assumes you have a `createdBy` set to uid
function isOwnerBefore() {
return request.auth.uid == resource.data.createdBy
|| resource == null;
}
function isOwnerAfter() {
return request.auth.uid == request.resource.data.createdBy
|| request.resource == null;
}
đź“ť Checking for null
before and after makes the function usable in more places with write
instead of a separate create
and update
.
Logged In#
function isLoggedIn() {
return request.auth != null;
}
Verifying Keys#
You can verify a document only contains certain fields or that only certain fields can be updated. First, you must get the keys()
from the map
type.
function hasAll(data, fields) {
// data has all fields
return data.keys().hasAll(fields);
}
function hasOnly(data, fields) {
// data has only fields
return data.keys().hasOnly(fields);
}
function isValidUserDocument() {
let fields = ['title', 'slug', 'minutes', 'content'];
return hasAll(request.resource.data, fields);
}
đź“ť Arrays are lists
in Firebase Rules.
List Methods#
concat
hasAll
hasAny
hasOnly
join
removeAll
size
toSet
- a unique set of lists
Map Methods#
diff(map_to_compare)
get(key, default_value)
keys()
size()
values()
String Verification Example#
Strings should be verified as well.
// verify the content is string type and at least 2 characters
&& request.resource.data.content is string
&& request.resource.data.content.size() > 2
String Methods#
lower()
matches(re)
replace(re, sub)
size()
split(re)
toUtf8()
trim()
upper()
Slug Example#
You could verify a slug, although this might be better handled in a trigger write function.
// verify slug = a slugify(title)
// should be updated for utf8 characters
request.resource.data.slug == request.resource.data.title
.lower().replace('[^a-z0-9]+', '-').replace('^-|-$','')
Other Types#
- You can view the Firestore Rules Index of all functions and operators for all types.
Reading is Expensive#
⚠️ Always use the current document from the request.resource
, and resource
variables. If you need to compare other documents, it will cost one read for each document. You should also consider custom claims instead of reading the user document from request.auth.token
. These are all free, as you are already reading the document outside Firestore Rules.
Reading Firestore External Documents#
You can compare other documents with four functions when it can't be avoided.
get(path)
- document before writegetAfter(path)
- document after writeexists(path)
- orget(path) != null
whether document exists before writeexistsAfter(path)
- orgetAfter(path) != null
whether document exists after write
allow write: if exists(/databases/${database)/documents/collectionName/documentID)
Pricing#
- You are only charged one read for multiple requests to the same document.
- You are only charged one read for the same document in a transaction or batch.
- See StackOverFlow Answer by Firebase Staff
- Some document access calls may be cached and don’t count against you
Clean Code and Limitations#
You need to model your data correctly to avoid the least amount of reads, keep your data consistent, and not have too much hassle. This requires you to know Firestore well, when to use Functions or the Server Environment, and how to write clean code.
Reusable Functions#
I personally avoid using variables in the match path. You can get the data you need from the request
and resource
variables anywhere in your code; there is no reason to create extra function parameters for global variables.
Notable Function Limitations#
- Maximum of 7 function arguments
- Maximum of 10 let variables per function
- Maximum of 20 function call depth
- Maximum of 1000 expressions evaluated per request
No Loops#
You cannot do loops or recursion in Firestore. This means you must set verifiable limits to verify a data set like tags
. Your function would need to run a string test 1-5 times, for example. Otherwise, you would use Firebase Functions to prevent problematic documents (after the write).
Match#
Put all your match
statements in one place, usually at the top or bottom of the document. The top will allow you less scrolling to see what you need. I like to use one function for every match call to keep in line with the Single Responsibilty Principle and to have cleaner code. However, you must keep track of the function dependencies to not reach the Firestore Rules Limitations.
match /databases/{database}/documents {
// MATCHES
match /users/{document} {
allow read, write: if allowUser();
}
// FUNCTIONS
function allowUser() {
return isLoggedIn() && isOwner()...
}
...
}
Writing Firebase Security Rules is an art.
J