Centralize Firestore Security Rules with Reusable Access Functions
Centralize Firestore security rules with reusable functions to enforce owner-based access in multi-tenant documents without duplicating logic.
30 Jul 2025, 06:37 UTC

One missed rule can expose every tenant’s data
You add an invoices collection to a multi-tenant SaaS, copy the read rule from projects, and forget the team-membership check. Suddenly any authenticated user can list every invoice. The failure is usually not a missing rule; it is duplicated logic that drifts apart as collections grow.
Firestore security rules evaluate request.auth.uid against fields in resource.data for every read and write. When that comparison is repeated across ten collections, a single new field name or overlooked condition creates a hole.
Centralize access checks in helper functions
Define the shared logic once at the top of your rules file, then call it from each collection’s match block. This keeps the rule body short and makes the ownership policy visible in one place.
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOwner(resource, uid) {
return resource.data.ownerId == uid;
}
function isTeamMember(resource, uid) {
return uid in resource.data.teamIds;
}
function isPublic(resource) {
return resource.data.visibility == 'public';
}
match /projects/{projectId} {
allow read: if isSignedIn() && (isOwner(resource, request.auth.uid) || isTeamMember(resource, request.auth.uid) || isPublic(resource));
allow write: if isSignedIn() && isOwner(resource, request.auth.uid);
}
match /invoices/{invoiceId} {
allow read: if isSignedIn() && (isOwner(resource, request.auth.uid) || isTeamMember(resource, request.auth.uid));
allow write: if isSignedIn() && isOwner(resource, request.auth.uid);
}
}
}
Here resource.data.teamIds is an array of user IDs belonging to the tenant. The in operator checks whether the current user’s UID appears in that list. Custom claims such as request.auth.token.role can add admin overrides, but they are separate from the document-level ownership check.
Worked example: a multi-tenant project document
Imagine a project document with this shape:
{
ownerId: "alice_123",
teamIds: ["alice_123", "bob_456"],
visibility: "private",
createdAt: ...
}
For bob_456, isOwner returns false but isTeamMember returns true, so a read is allowed. A write is denied because Bob is not the owner. For an unauthenticated request, isSignedIn fails immediately and both read and write are denied.
Trade-offs and how to verify before deploying
Richer rules cost more evaluation time. Avoid get() calls across collections inside hot paths; they add latency and can hit rule evaluation limits. Keep conditions based on the current document’s fields whenever possible. Rule changes also take up to a few minutes to reach live clients, so do not assume an edit is instant.
Test locally with the Firebase Emulator Suite before touching production:
- Run
firebase emulators:start --only firestorein your project directory. You need the Firebase CLI and read access to local project files; no production permissions are required. - Write unit tests with
@firebase/rules-unit-testingthat simulate requests as different UIDs and assert allow or deny outcomes. - Use the Firestore security rules playground in the Firebase Console to inspect the evaluation trace for a specific UID and document.
- After deploying to a test project, monitor Usage > Firestore > Security rules in the console for spikes in average evaluation time.
Deploy with:
firebase deploy --only firestore:rules --project <PROJECT_ID>
This requires an Owner or Editor role in the Firebase project. A syntax error fails the deploy; a logic error can silently block legitimate users or expose data, so treat the rules file like application code and review it in pull requests.
Rollback and closing checklist
Because deploying rules changes state, keep the previous firestore.rules in version control. If the new rules break access, redeploy the earlier file with the same deploy command. There is no automatic “undo” for rule propagation delay.
Before you ship: extract every repeated ownerId comparison into a named function, cover each collection with emulator tests, and confirm the evaluation trace in the playground. Centralized rules will not fix bad logic by themselves, but they make the logic easier to find, review, and correct.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.