Diagnosing Firestore 'permission-denied' Errors Caused by Security Rules Misconfiguration
A step‑by‑step guide to identify why Firestore SDK calls return 403/permission‑denied, map the symptom to common rule faults, verify each hypothesis, apply the fix, and know when to escalate.
19 Jul 2026, 00:25 UTC

Recognizable condition
When a Firestore operation (read, write, list, or get) fails, the client SDK throws a FirebaseError with code permission-denied (or the REST API returns HTTP 403). The error message typically contains the phrase "Missing or insufficient permissions". This symptom appears consistently for a specific document path or collection, while other paths may work correctly.
Cause & diagnostic table
| Possible cause | What it looks like in the rule set | Typical clue |
|---|---|---|
| Rule syntax error | Rules deploy successfully but the logic evaluates to false for the request (e.g., a typo in a function name). | Rules Playground shows a denial even though the intent was to allow. |
| Auth context mismatch | request.auth is null or the UID does not match the expected value in the resource path. | User is not signed in, or a custom claim used in the rule is missing. |
| Resource field access | Rule references a field that does not exist on the document (e.g., resource.data.role when the document has no role field). | Error occurs only on documents lacking that field; adding the field makes the request succeed. |
| Wildcard overreach | A broad match /{doc=**} grants read but the corresponding write rule is missing or overly restrictive. | Read succeeds, write fails on the same path. |
| Inherited deny | A parent path rule returns false (or deny) and child rules cannot override it. | Any request to a subcollection is denied regardless of child rules. |
| Time‑based rule | Comparison using request.time fails because the client clock is skewed or a server timestamp is used incorrectly. | Error appears intermittently and correlates with device time. |
Ordered verification steps
- Inspect the deployed rule version
Open Firebase Console → Firestore → Rules tab. Note the timestamp and version number displayed at the top.
- Use the Rules Playground to simulate the failing request
Select the exact method (get, create, update, delete, list), paste the full document path, and fill in the
request.authobject with the UID (ornull) you observe in the client. Click “Simulate”. Record whether the result isallowordenyand which expression evaluated to false. - Enable debug logging on the client
For Web:
firebase.firestore().setLogLevel('debug');
For iOS:FirebaseConfiguration.shared.setLoggerLevel(.debug);
For Android:FirebaseFirestore.setLoggingEnabled(true).
Reproduce the operation and look in the console/network tab for lines likefirestore.googleapis.com%2Fsecurity_rulesthat show the evaluated rule expression. - Verify the auth state at the moment of the call
Web:
firebase.auth().onAuthStateChanged(user => { console.log(user?.uid); });
iOS:Auth.auth().addStateDidChangeListener { _, user in print(user?.uid ?? "nil") }
Android:FirebaseAuth.getInstance().addAuthStateListener { _, user -> Log.d("Auth", user?.uid ?: "null") }.
Ensure the UID matches what you used in the Playground. If using custom claims, callgetIdTokenResult(true)(force refresh) and inspect theclaimsfield. - Check the actual document shape
Before a write, read the document (if it exists) and log its data:
docSnap.data(). Confirm that any fields referenced in the rule are present and have the expected type. - List all matching rules for the collection/path
In the Rules editor, search for
matchstatements that could apply to the path. Note any overlappingallowordenyrules and the order in which they are evaluated.
Fixes tied to findings
- Rule syntax error
Correct the typo or logic, then redeploy:
firebase deploy --only firestore:rules. Ensure you have theOwnerorEditorrole on the project. - Auth context mismatch
Make sure the sign‑in flow completes before any Firestore call. Wrap reads/writes in an
onAuthStateChanged or equivalent. If you rely on a custom claim, sign the user out and back in after the claim is set, or callgetIdTokenResult(true)to force a token refresh. - Resource field access
Either add the missing field to the document schema or make the rule tolerant with optional chaining:
resource.data?.role == 'admin'. After editing the rule, redeploy as above. - Wildcard overreach
Add an explicit write condition for the wildcard path, e.g.:
match /{doc=**} { allow read: if request.auth != null; allow write: if request.auth != null && request.auth.uid == resource.data.ownerId; }Redeploy the rule set.
- Inherited deny
Refactor the rule hierarchy so that denies are not placed on parent paths unless intended. Use a shared function for common conditions and call it from child matches:
function isSignedIn() { return request.auth != null; } match /users/{userId} { allow read, write: if isSignedIn(); match /posts/{postId} { allow read, write: if isSignedIn() && request.auth.uid == userId; } }Redeploy.
- Time‑based rule
Replace client‑derived timestamps with server timestamps when checking expiration or validity:
allow write: if request.time > resource.data.expiresAt; // where expiresAt is a Timestamp stored on the documentAvoid using
request.timefor comparisons that rely on device clock; if you must, allow a tolerance (e.g.,abs(request.time.timestampSeconds - now.timestampSeconds) < 300). Redeploy after changes.
Escalation criteria
- Rules Playground passes but client still fails
This often points to SDK cache or offline persistence. Clear the persistence layer: Web:
await firebase.firestore().disableNetwork(); await firebase.firestore().enableNetwork();or callfirebase.firestore().clearPersistence()(if you are okay with losing cached data). On Android/iOS, callFirebaseFirestore.getInstance().clearPersistence(). Then re‑authenticate and retry. - Multiple developers see inconsistent results
Check that each developer’s FirebaseApp initialization uses the same project ID. Look for multiple
initializeAppcalls with differentprojectIdvalues. Ensure only one Firebase app is active per process. - Error persists after rule fix and redeploy
Rule changes normally propagate within 60 seconds. Wait, then repeat the Playground simulation. If denial continues, collect:
- Project ID
- Timestamp of the failed request (from client logs)
- Exact request payload (path, method, auth UID)
- Exported Rules Playground JSON (use the “Export” button)
Verification
- Run the Rules Playground simulation with the exact failing request parameters; confirm the result matches the expected
allowordeny. - Add a temporary log statement in the rule:
log('debug', 'uid', request.auth.uid);. After redeploy, view the logs in Cloud Logging under the filterresource.type="firestore_database" AND logName="projects/[PROJECT_ID]/logs/firestore.googleapis.com%2Fsecurity_rules". - Write an integration test using the local emulator and
@firebase/rules-unit-testing(Node) to exercise the rule against a known document shape. - Monitor Firebase Console → Firestore → Usage → “Permission denied” count; verify it drops after the fix is deployed.
- Reproduce the original user flow with debug logging enabled; ensure the network tab shows no 403 responses for the previously failing operation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.