Stopping the 'Open Door' Policy: Securing Firestore with Granular Rules
Stop using recursive wildcards in Firestore. Learn how to bind user identity to document paths to prevent unauthorized data access and move beyond simple authentication.
07 Jun 2026, 23:32 UTC

The Danger of the Global Wildcard
Many developers start their Firebase projects with a rule set that looks like this: match /{document=**} { allow read, write: if request.auth != null; }. While this ensures only logged-in users can access the database, it creates a massive security hole. Any authenticated user can read or overwrite any other user's data, provided they know the document ID.
The core problem is that {document=**} is a recursive wildcard. It applies to every single collection and sub-collection in your database. To move from "authenticated access" to "authorized access," you must shift from global rules to path-specific constraints.
Binding Identity to Document Paths
The most effective way to secure user data is to mirror your database structure in your rules. By using a named wildcard in the match path, you can bind the document ID to a variable and compare it against the authenticated user's UID.
When a request hits Firestore, the server evaluates the request.auth object. If the uid in that object matches the userId variable extracted from the URL path, access is granted. This ensures that User A cannot access /users/UserB/private_settings even if they are logged in.
Practical Implementation: User-Owned Data
Consider a scenario where you have a users collection and a sub-collection called notes. You want users to manage their own profiles and notes, but you want a separate public_profiles collection that anyone can read.
service cloud.firestore {
match /databases/{database}/documents {
// Public profiles: Anyone can read, only the owner can write
match /public_profiles/{userId} {
allow read: if true;
allow write: if request.auth != null && request.auth.uid == userId;
}
// Private user data: Only the owner has any access
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
// Nested match for sub-collections
match /notes/{noteId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
}How to verify this configuration
To test these rules without risking production data, run the Firebase Emulator Suite on your local machine. This allows you to simulate different user IDs and verify that access is denied when the UID does not match the path.
# Run this in your project root terminal
firebase emulators:start --only firestore,authCheck: Attempt to fetch a document from /users/user_abc while authenticated as user_xyz. The SDK should return a PERMISSION_DENIED error.
The Trade-off: Rule Complexity vs. Performance
As your application grows, you might be tempted to use get() or exists() calls within your rules to check permissions stored in other documents (e.g., checking if a user is an 'admin' in a roles collection).
While powerful, these cross-document lookups incur additional read costs and can slightly increase latency. If you find yourself performing multiple get() calls per request, consider flattening your data or using Custom Claims in Firebase Auth to store roles directly on the user's identity token.
Deployment and Rollback
Updating security rules is an atomic operation; the new rules apply to all requests immediately upon deployment. However, because a typo in a rule can lock out your entire user base, always verify with the emulator first.
If a deployment causes unexpected PERMISSION_DENIED errors in production, you can roll back by deploying the previous version of your firestore.rules file via the CLI:
# Deploy specific rules file to restore previous state
firebase deploy --only firestore:rules0 replies
A thoughtful contribution can make all the difference. Be the first to share one.