Firestore PERMISSION_DENIED: Every Real Cause and Fix

Firestore PERMISSION_DENIED: Every Real Cause and Fix

# firebase# firestore# securityrules# debugging
Firestore PERMISSION_DENIED: Every Real Cause and FixMahdi BEN RHOUMA

Fix firestore PERMISSION_DENIED: locked-mode rules, undeployed rules, request.auth null, wildcard mismatches and queries broader than your rules allow.

You add Firestore to a project, write a document, and the very first call fails:

// Web SDK v9+ (modular)
import { initializeApp } from 'firebase/app';
import { getFirestore, collection, addDoc } from 'firebase/firestore';

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

await addDoc(collection(db, 'notes'), { text: 'hello' });
Enter fullscreen mode Exit fullscreen mode
FirebaseError: Missing or insufficient permissions.
  code: "permission-denied"
Enter fullscreen mode Exit fullscreen mode

On Android the same failure reads PERMISSION_DENIED: Missing or insufficient permissions, which is exactly the wording of the canonical Stack Overflow thread that has collected answers for this error since Firestore launched. The thread is long precisely because the message is opaque by design: Firestore will not tell a client which rule rejected it, because that would leak information about your security model. So the fix is never "read the error more carefully" — it is "work out which rule was evaluated".

Read the rule Firestore actually evaluated

Before changing anything, reproduce the denial somewhere that does show you the evaluation. You have three options, in increasing order of fidelity:

  1. The Rules Playground in the Firebase console (Firestore → Rules → Rules Playground). Simulate the exact operation — get, list, create, update, delete — against the exact document path, with or without an authenticated UID. It highlights the specific allow statement that matched, or shows that none did. This is the fastest way to distinguish "my rule is wrong" from "my rule is fine but my request does not match it".
  2. The Local Emulator Suite (firebase emulators:start). The emulator logs every rules evaluation with the outcome, so you can replay your real application code against it.
  3. The Firestore debug console output on the client: log error.code and the full path you attempted. permission-denied from the client SDK always means rules; the same code from a server SDK means something else entirely (see the Admin SDK section below).

Two facts from the official docs shape everything that follows. First, rule deployments are not instant: updates take up to a minute to affect new queries, and up to ten minutes to propagate to active listeners. Second, rules apply only at the matched path and do not cascade — a rule on /cities/{city} says nothing about /cities/{city}/landmarks/{landmark}.

With that methodology in hand, the causes sort cleanly.

Symptom → cause → fix

Symptom Likely cause Fix
Every read and write fails, brand-new project Production-mode default rules: allow read, write: if false; Write real rules and deploy them
Everything worked for 30 days, then all requests fail Test-mode rules expired (request.time < timestamp.date(...)) Replace the time-boxed rule with auth-based rules
Rules look correct in your editor, requests still fail Rules never deployed, or deployed < 1 min ago firebase deploy --only firestore:rules, then wait a minute
Fails only when signed out, or fails immediately on page load request.auth == null — auth state not yet resolved Gate Firestore calls behind onAuthStateChanged
Single get() works, collection query fails Query broader than the rule allows — rules are not filters Add a where() clause that mirrors the rule condition
Works on /users/{userId}, fails on a subcollection Rules do not cascade to subcollections Add a match block (or recursive wildcard) for the subpath
You edited rules in the console but the app uses different ones You edited Realtime Database rules, not Firestore rules Edit Firestore → Rules, not Realtime Database → Rules
Server-side code fails with PERMISSION_DENIED Admin SDK bypasses rules — this is IAM, not rules Fix the service account's IAM role / enable the Firestore API

The rest of this article works through each fix with the exact rule code involved.

Fix 1: the locked-mode default is deny-all

When you create a Firestore database in production mode, the getting-started documentation shows the default ruleset you receive:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

if false denies everything, for everyone, including signed-in users. This is the single most common cause on new projects. The minimal sensible replacement — access for authenticated users only — is the docs' own example:

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Resist the temptation to use if true to "get it working". The docs warn that an open ruleset lets anyone read and overwrite your entire database, and test-mode databases ship with a request.time < timestamp.date(...) clause for exactly this reason — which brings its own failure mode: thirty days later, every request in your app starts failing at once. If your app broke on a schedule, check whether your rules contain an expired timestamp.

Fix 2: rules you never deployed do not exist

Editing firestore.rules in your repository changes nothing until you deploy it:

firebase deploy --only firestore:rules
Enter fullscreen mode Exit fullscreen mode

Deploying from the CLI overwrites whatever was last saved in the console editor, and vice versa — pick one source of truth. And remember the propagation window from the docs: up to a minute for new queries, up to ten minutes for active listeners. A fix that "did not work" fifteen seconds after deployment may simply not have arrived yet.

Fix 3: request.auth is null

Any rule containing request.auth != null or request.auth.uid denies unauthenticated requests. Several answers on the Stack Overflow thread reduce to the same race: the app queries Firestore before Firebase Auth has restored the session. On the web SDK, auth state restoration is asynchronous — a query fired at module load runs as an anonymous request even for a signed-in user. Gate your first query:

import { getAuth, onAuthStateChanged } from 'firebase/auth';

onAuthStateChanged(getAuth(), (user) => {
  if (user) loadUserData(user.uid); // safe: request.auth is populated
});
Enter fullscreen mode Exit fullscreen mode

Fix 4: your match block does not cover the path

Firestore rules match documents, not collections, and they do not cascade. Given:

match /users/{userId} {
  allow read, write: if request.auth.uid == userId;
}
Enter fullscreen mode Exit fullscreen mode

a write to /users/abc/orders/123 is denied, because no rule matches that path — the users rule stops at the user document. You need either an explicit nested block or a recursive wildcard:

match /users/{userId}/{document=**} {
  allow read, write: if request.auth.uid == userId;
}
Enter fullscreen mode Exit fullscreen mode

Under rules_version = '2', {document=**} matches zero or more path segments, so this one block covers the user document and everything beneath it.

One adjacent trap: Firestore and the Realtime Database are different products with different rule languages and different editors in the console. Firestore rules start with service cloud.firestore; Realtime Database rules are JSON. If you pasted correct-looking rules and nothing changed, confirm you were in Firestore → Rules and not Realtime Database → Rules.

Fix 5: rules are not filters — broad queries fail whole

This is the least intuitive cause, and the official query documentation is blunt about it: "security rules are not filters — queries are all or nothing." Firestore evaluates a query against its potential result set. With this rule:

allow read: if request.auth != null
            && request.auth.uid == resource.data.author;
Enter fullscreen mode Exit fullscreen mode

this query fails —

db.collection('stories').get(); // PERMISSION_DENIED
Enter fullscreen mode Exit fullscreen mode

even if the current user is the author of every single story in the database, because the query as written could return someone else's documents. The fix is to constrain the query so it provably satisfies the rule:

db.collection('stories').where('author', '==', user.uid).get(); // allowed
Enter fullscreen mode Exit fullscreen mode

If a single doc().get() succeeds but the collection query fails, this is almost certainly your cause.

Fix 6: the Admin SDK never sees your rules

The getting-started docs state that the server client libraries bypass Cloud Firestore Security Rules entirely. This cuts both ways:

  • You cannot reproduce a client-side permission-denied with the Admin SDK, and no rules change will ever affect Admin SDK behaviour.
  • If your server code returns PERMISSION_DENIED, stop reading your rules file. The problem is Google Cloud IAM — the service account lacks a Firestore role — or the Cloud Firestore API is disabled on the project. Several answers on the Stack Overflow thread resolve exactly this by fixing the API enablement in the Cloud console.

Knowing which SDK produced the error tells you which of two completely different systems denied it.

How Supabase RLS makes this failure mode explicit

If you have followed the debugging path above, you have felt the core friction: Firestore's security model lives in a separate DSL, evaluated remotely, with a deliberately uninformative error. The Postgres world solves the same problem — per-row access control — with Row Level Security, and the failure surface is different in ways worth knowing if you ever run both stacks.

With Supabase RLS, a denied read does not throw at all: the rows are silently filtered out of the result, because RLS policies genuinely are filters — the exact opposite of Firestore's all-or-nothing query evaluation. A denied write fails with a named Postgres error (42501, or a policy violation message naming the table), and because policies are SQL, you can test them directly in the SQL editor with set role — no playground or emulator required. Our Supabase RLS debugging checklist is the RLS equivalent of this article's methodology section, and the 42501 permission denied guide covers the schema-level grants that sit underneath policies — the layer Firestore simply does not expose.

The trade-off is real in both directions. Firestore's opaque permission-denied is annoying to debug but leaks nothing; RLS's silent filtering can hide bugs where a policy quietly excludes rows you expected. For multi-tenant designs, where Firestore forces the where('tenant', '==', ...) pattern from Fix 5 onto every query, RLS lets the database enforce tenancy invisibly — the approach we detail in the multi-tenant SaaS architecture guide. If you are debugging access control across either stack, the Supabase debugging hub collects every related fix in one place.


Originally published at https://www.iloveblogs.blog