Guides
Firestore security rules cheatsheet: 12 patterns you'll actually use
ยท8 min read
Security rules are the only thing standing between your Firestore database and the public internet. Here are the patterns that cover 90% of real apps, with copy-paste examples.
The mental model
Firestore security rules are not middleware. They run on every read and write, they cannot call out to your backend, and they evaluate per-document. Think of them as a pure function from (request, resource) to allow or deny. If a rule needs data it doesn't have, the request is denied, full stop.
Two more things to internalize: rules do not filter, they reject; and a get() inside a rule counts as a read you pay for. Design accordingly.
Pattern 1: lock everything by default
Start every ruleset with a default-deny match on /{document=**}. Then open up specific paths. The number one cause of public Firestore leaks is a rule that allowed too much because the developer forgot the default.
Pattern 2: signed-in only
allow read, write: if request.auth != null. Useful for app-wide data that any logged-in user should see, like a list of public channels.
Pattern 3: owner only
Match on /users/{userId}/{document=**} and allow read, write: if request.auth.uid == userId. The cleanest pattern in Firestore, and the reason user-scoped data usually lives in a subcollection.
Pattern 4: role-based access
Store roles in a separate /roles/{uid} document and read them with get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role == 'admin'. Cache the role in custom claims if you can, that avoids the extra read on every request.
Pattern 5: public read, authenticated write
allow read: if true; allow write: if request.auth != null. Common for blog posts, product catalogs or anything you want crawlable.
Pattern 6: validation
Validate writes with request.resource.data checks: required fields, types, max lengths, allowed enum values. This is where most rules get long, and where most bugs hide.
Pattern 7: immutable fields
allow update: if request.resource.data.createdAt == resource.data.createdAt. Pin createdAt, ownerId or anything else that should never change after creation.
Pattern 8: rate limiting per user
Combine a counter document with request.time and a rule that checks elapsed time since the last write. Crude but effective for abuse prevention without a backend.
Pattern 9: shared resources
Store an array of member UIDs on the parent document and allow access if request.auth.uid in resource.data.memberIds. Works well up to a few hundred members; beyond that, use a members subcollection.
Pattern 10: server-only collections
Set allow read, write: if false. The collection is only writable from the Admin SDK via a server function. Use this for credentials, audit logs and anything sensitive.
Pattern 11: time-bound access
allow read: if resource.data.expiresAt > request.time. Great for invitations, signed links and trial features.
Pattern 12: tenant isolation
Prefix every collection with /tenants/{tenantId}/ and gate on a tenantId claim in the auth token. The cleanest multi-tenant pattern in Firestore.
Testing your rules
Use the Firebase emulator's rules unit tests for anything beyond trivial. Rules are code; they deserve assertions. A Firestore GUI like FireSheets is handy for seeing the data shape your rules will actually receive, which makes writing the validation pattern much easier.
Try FireSheets free
Connect a Firebase project in under a minute. Free forever for solo builders, no credit card required.
Get started
FireSheets