Security Rules
Firestore has no application server of its own that you control. Any client holding your project config talks straight to the database, so firestore.rules is the only thing standing between a user and every document in the project. It is not a convenience layer — it is the authorization layer, and by default it lives in a separate file written in a separate language from the models it protects.
Why security rules drift out of sync
The rules file describes the same collections and the same fields as your models, but nothing keeps the two descriptions honest. You rename a field, add a required field, or introduce a subcollection, and the rules file happily keeps referring to the old shape. Stale rules do not fail loudly — they either keep allowing writes that should now be rejected, or they start rejecting legitimate reads with PERMISSION_DENIED: Missing or insufficient permissions, which surfaces in the client and looks like an authentication bug rather than a rules bug.
Two Firestore behaviours make this worse than it sounds. Rules are not filters: if a query could match even one document the rules cannot prove is readable, Firestore rejects the entire query instead of returning a subset. And rules do not cascade — a match block on /users/{id} says nothing about documents underneath it, so a locked-down parent can sit above a wide-open subcollection. Declaring rules on the model that owns the collection puts them in the same file, the same diff, and the same review as the schema change that made them necessary.
Declaring rules on a model
Three shortcut decorators cover the access patterns most collections actually need. Anything else goes through @Rules, which takes one expression per operation.
// Shortcut decorators
@AuthRequired // auth != null
@AuthOwner // auth.uid == resource.data.uid
@PublicRead // open read, auth write
// Custom rules
@Rules({
read: 'auth != null',
write: 'auth.uid == resource.data.uid',
create: 'auth != null && request.resource.data.keys().hasAll(["name"])',
})@AuthRequired grants read and write to any signed-in user — a reasonable default for data shared across a workspace but not meant to be public. @AuthOwner narrows both operations to the document owner by comparing the caller against a uid field stored on the document itself, which means the collection has to actually carry that field. @PublicRead opens reads to unauthenticated clients and keeps writes behind sign-in — useful for catalogue-style data, as long as you remember that public read means public to scrapers, not just to your UI.
The @Rules object accepts read, write, create, update and delete. The narrower operations win over write, which fills in for whichever of the three you leave out — so the example above ends up with the stricter condition on creates and the owner check on updates and deletes. The difference between resource.data and request.resource.data matters here: the first is the document as it currently exists, the second is the document the caller is trying to write. On a create there is no existing document, so an owner check written against resource.data rejects every insert — one of the most common reasons a create fails while updates work fine.
Field validation comes along with it
The generator does not only emit your access expressions. It also reads the field decorators on the model and folds validation into the create rule: a keys().hasAll([...]) check covering every field marked @Required, plus a type assertion per field. That closes the gap between client-side validation and what the database will actually accept — a caller hitting the Firestore REST API directly is held to the same constraints as your app. Models declared with a parent collection get the nested match path with the parent wildcard, so the parent's rules and the child's remain independent, as Firestore requires.
Generating and deploying
Run the generator to write firestore.rules into your configured output directory, then deploy it with the Firebase CLI like any other config file. Pass --dry-run to print the file to stdout without touching disk when you only want to see what changed.
firemap generate:rules
firemap generate:rules --dry-run
firebase deploy --only firestore:rulesTwo caveats worth internalising. Rules deploy atomically and take effect within seconds, so a bad deploy locks out live users immediately — run the change against the Firestore emulator first. And rules are bypassed entirely by the Firebase Admin SDK: anything running with service account credentials has full access no matter what this file says, which is why server-side code still needs its own authorization checks. See the CLI reference for running this alongside the index and Cloud Function generators.