After using firestore for a larger project and synthesizing some best practices into my use cases, I have my way of explaining design choices.
When drilling into layers of the db, you have to alternate between collections (typically arrays of generated ids) and documents (dictionaries).
Top level is a collection of collections.
Then in that list you have your drill down into a single collection.
A single document in a collection can have dictionary data, and it can contain more collections.
Or in other words:
document:
dictionary data
collection1 (named id)
collection2
collection3
...
collection:
document1 (named id, or generated ids)
document2
document3
...
Then when you drill thru layers of your db a pattern emerges in the paths:
dictionary_data = db.collection("toplevel1").document("document1").collection("nested-collection1").document("nested-document1").get()
Collection, Doc, Collection, Doc, Collection, Doc, etc.
Just make a collection and use add
and include a timestamp. If you use a Firestore style timestamp,
it is easily readable in the admin db editor.
const modified = firebase.firestore.Timestamp.now()
// or
const modified = firebase.firestore.Timestamp.fromDate(new Date())
// or
import { serverTimestamp } from "firebase/firestore";
timestamp: serverTimestamp()
For a multitenant architecture, having the data structured well initially helps a lot.
/users/{userId}/
user-data (dict)
/teams/{teamId}/projects/{projectId}/
project-data (dict)
/metadata-teams/{teamId}/transactions/{transactionId}/
transaction-data (dict)
/metadata-projects/{projectId}/
project-metadata (dict)
team-metadata (dict)
/history-teams/{teamId}/history-projects/{projectId}/{history-type}/
history-data (dict)
project-history-data (dict)
/feedback/{issueId}/
issue-data (dict)
Including a modified
and modifiedBy
stamp on each level makes reports a lot easier later.
Shape of the db is strongly related to roles or things accessible by who or which group of users or backend only.
Also don't duplicate collection names if possible... This makes for cross db collection group queries easier.
If you missed this design step earlier, then you can work around it by adding a boolean into each document in a specific directory to describe where it is in the dictioanry.
E.g.
/metadata-teams/{teamId}/projects/{projectId}
"metadataTeamsProjects": true
and
/teams/{teamId}/projects/{projectId}
"teamsProjects": true
https://firebase.blog/posts/2019/06/understanding-collection-group-queries
When writing firestore security rules, the match
syntax is unaware of the dictionary data.
It is only aware of the collection names and the document names.
This makes it so that when you are first writing rules it can be easy to think,
"I'm drilling thru all my levels of my nested dictionary including my final dict layer".
WRONG. The final layer is referenced as incomingData()
or request.resource.data
.
The wild card of {document**}
with rules v2, doesn't touch the final dict layer, only the collection names and document names.
The blog from google discussing the higher precision for filtering on incoming data, too, makes this topic more clear.
https://firebase.blog/posts/2021/01/code-review-security-rules
Be sure to plan a way to associate a user to their roles in the custom claims stored in the login token. The verification of claims in the firebase rules is quick and easy.
Setting a claim needs to be done by an admin sdk such as a backend or in the firebase functions.