Firebase Security Rules For Multi-Tenant Apps
A practical guide to writing Firestore security rules that enforce tenant isolation, role-based access, and data integrity without a custom backend.
The hidden risk of default Firestore rules
Firebase makes it incredibly easy to ship a working app in days. But the default security rules — especially the "test mode" rules that allow all reads and writes — are a ticking time bomb. I have personally audited three client projects where production Firestore databases were wide open to the internet because nobody changed the default rules after launch.
The first rule of Firebase security: your Firestore rules ARE your backend authorization layer. If you skip them, you have no authorization at all, regardless of what your frontend code checks.
Tenant isolation with document-level rules
For multi-tenant apps like Gene Pharmacy POS, every document in the database includes a tenantId field. The security rule checks that the authenticated user's custom claim (set during sign-up by a Cloud Function) matches the document's tenantId. This prevents Pharmacy A from reading Pharmacy B's inventory or sales records.
The rule pattern looks like: allow read, write: if request.auth.token.tenantId == resource.data.tenantId. Simple, but it must be applied to every single collection and subcollection — missing even one collection creates a data leak.
Role-based write restrictions
Beyond tenant isolation, we enforce role-based writes. A cashier can create sale records but cannot modify product prices. A manager can update prices but cannot delete financial ledger entries. An admin can do both but is still prevented from modifying audit logs.
These rules are enforced in Firestore security rules using custom claims: allow write: if request.auth.token.role in ["admin", "manager"] for price updates, and allow delete: if false for audit collections (nobody can delete audit logs, not even admins).
Validating data shape in rules
Firestore rules can also validate the shape of incoming data. We check that required fields exist, that numeric values are positive, that string fields do not exceed maximum lengths, and that status fields contain only valid enum values. This prevents both accidental bugs and deliberate data injection.
Example: allow create: if request.resource.data.keys().hasAll(["productId", "quantity", "unitPrice"]) && request.resource.data.quantity > 0. This is not a replacement for frontend validation, but it is the last line of defense before data hits the database.
Designing Secure APIs For Real Operations
Why consistent contracts, permissions, and structured failure handling matter more than flashy endpoint counts.
Read ArticleRBAC Design For Business Systems
A practical approach to authorization when your product has admins, reviewers, operators, and stakeholders with different responsibilities.
Read Article