All posts

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

Keep reading