Fix this error · Supabase and Firebase
Fix: FirebaseError: Missing or insufficient permissions.
Fix Firestore permission-denied: query after onAuthStateChanged, rules that match the path, queries that match the rules, and onSnapshot error callbacks.
Cloud Firestore rejected the read or write because your security rules did not allow it for this request. The most common reason in web apps is timing: the query runs before Firebase Auth has restored the user, so request.auth is null and a rule like request.auth.uid == userId fails. Wait for onAuthStateChanged before you query, and make sure the rule matches the exact path and the query's filters.
| Fact | Detail |
|---|---|
| Error | FirebaseError: Missing or insufficient permissions. |
| Error code | permission-denied |
| Where you see it | Browser console, the rejected promise from getDoc / setDoc / getDocs, the error callback of onSnapshot |
| Usual cause | The request runs before sign-in resolves, or the rules do not cover that path or query |
| Usual fix | Query after onAuthStateChanged gives you a user, and write rules that match the path and the query |
| BugTape catches it | Yes, automatically, when the rejection is unhandled. A listener without an error callback only logs it |
What the error means
Every request from the web, Android or iOS client SDKs is checked against your Firestore security rules. When no allow rule grants the operation, Firestore refuses it with the code permission-denied and the message Missing or insufficient permissions. The SDK raises a FirestoreError with that code. Its toString() is FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
The message does not say which document or rule failed. The server client libraries and the Admin SDK bypass security rules and use IAM instead, so this error comes from client code.
Common causes
- The query runs before Auth is ready. On page load,
auth.currentUseris null until Firebase Auth has finished initializing. A query sent in that window hasrequest.auth == null. - The rules do not match the path. A rule on
match /users/{userId}does not coverusers/{userId}/orders/{orderId}. Subcollections need their ownmatchblock or a recursive wildcard. - The query could return documents the user may not read. Rules are not filters. A query for the whole
postscollection fails when the rule only allowsresource.data.authorId == request.auth.uid, even if every stored post belongs to the user. Add the same condition to the query. - A write fails a condition on the new data.
request.resource.dataholds the document as it would be after the write. A missing or wrong field, such asownerId, fails the rule. - The rules were never deployed, or a test-mode rule has expired. Rules in your editor are not live until you publish them. If your rules contain
request.time < timestamp.date(...), every request is denied after that date. - Production mode defaults. A database created in production mode denies all reads and writes from mobile and web clients until you add rules.
- The user signed out while a listener was open. An
onSnapshotlistener on private data can fail once the user is signed out and the rules no longer allow the read.
How to fix it
Query after the user is known
Start reads in the onAuthStateChanged callback, so the request carries the signed-in user.
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, getDoc } from 'firebase/firestore';
const auth = getAuth();
const db = getFirestore();
onAuthStateChanged(auth, async (user) => {
if (!user) return; // show the signed-out state, do not query private data
const snap = await getDoc(doc(db, 'users', user.uid));
console.log(snap.data());
});In React, keep the user in state and run the query in an effect that depends on it.
'use client';
import { useEffect, useState } from 'react';
import { onAuthStateChanged, type User } from 'firebase/auth';
import { collection, query, where, onSnapshot } from 'firebase/firestore';
import { auth, db } from './firebase';
export function MyPosts() {
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<unknown[]>([]);
useEffect(() => onAuthStateChanged(auth, setUser), []);
useEffect(() => {
if (!user) return;
const q = query(collection(db, 'posts'), where('authorId', '==', user.uid));
return onSnapshot(
q,
(snap) => setPosts(snap.docs.map((d) => d.data())),
(err) => console.error(err.code, err.message),
);
}, [user]);
return <p>{posts.length} posts</p>;
}Returning the unsubscribe function closes the listener when the user changes or signs out.
Write rules for each path the app reads
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
match /orders/{orderId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
match /posts/{postId} {
allow read: if request.auth != null && resource.data.authorId == request.auth.uid;
allow create: if request.auth != null && request.resource.data.authorId == request.auth.uid;
}
}
}Publish with firebase deploy --only firestore:rules, or in the console under Firestore, Rules.
Make the query match the rule
The rule above allows a post only when authorId is the user. The query has to say the same thing, so Firestore can prove that every result is allowed.
// Fails: could return other users' posts
getDocs(collection(db, 'posts'));
// Works: matches the rule
getDocs(query(collection(db, 'posts'), where('authorId', '==', user.uid)));Send the fields the write rule checks
await setDoc(doc(db, 'posts', id), {
title,
authorId: auth.currentUser!.uid, // the rule checks request.resource.data.authorId
});Test the rule before you guess
Use the Rules Playground in the Firebase console, or the Firestore emulator, to run the same read as a given user and see which line denies it. Do not switch to allow read, write: if true. It removes the error by letting anyone read and change your data.
How to catch it in production with BugTape
Firestore answers through its own SDK, so do not expect a failed HTTP request in the report. What BugTape sees depends on how your code handles the error.
- A rejected promise you do not catch.
await getDoc(...)inside an effect or click handler with notry/catchbecomes an unhandled promise rejection. The browser SDK reports it automatically with the error message, the page URL, the stack and the steps the user took. - A listener without an error callback. Firestore logs
Uncaught Error in snapshot listener:to the console and nothing is thrown. BugTape records console output into a report, but console output does not start one. Add an error callback that callsreportError. - An error you catch and show. Nothing reaches BugTape unless you report it.
- Before
init(). A query that fails while the page is still loading, before BugTape starts, is missed.
The browser SDK has no captureException. reportError(err) fires the same window error event the SDK listens for:
onSnapshot(
q,
(snap) => setPosts(snap.docs.map((d) => d.data())),
(err) => {
if (err.code === 'permission-denied') reportError(err);
},
);Install the browser SDK once:
import { init, identify } from 'https://app.bugtape.ai/bugtape.mjs';
init({ apiKey: 'YOUR_PROJECT_KEY', release: 'YOUR_COMMIT_SHA' });
// after sign-in:
identify({ userId: user.uid });Calling identify() after sign-in records which user hit the error. A report with no user, right after page load, points at the Auth timing cause. Repeats on the same page group into one issue with a count. Set release so a rules or code fix that does not hold reopens the issue as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the change. You review the pull request.
FAQ
Why does it work after a refresh or a second click but fail on the first load?
The first request ran before Firebase Auth had restored the user, so request.auth was null. By the second try Auth was ready. Start the query from onAuthStateChanged, or wait for a user in your component state.
My rule only allows the user's own documents, and all the documents are theirs. Why does the query fail?
Rules are not filters. Firestore checks whether the query could return a document the user cannot read. It does not look at which documents are stored now. Add where('ownerId', '==', user.uid) (or whatever field the rule checks) to the query.
Does the Admin SDK get this error?
No. The server client libraries and the Admin SDK bypass security rules and use Google credentials and IAM instead. If you see this error, the request came from a client SDK, or from a server using the client SDK.
Can I find out which document failed?
The error does not include the path. Log the path next to each call in development, or wrap reads in a helper that adds it to the error before you report it.
Sources
- Firebase: Writing conditions for Cloud Firestore Security Rules (
request.auth, rules are not filters,request.resource.data), checked 2 Nov 2026. - Firebase: Get started with Cloud Firestore Security Rules (server client libraries bypass rules), checked 2 Nov 2026.
- Firebase: Manage users on the web (
onAuthStateChanged,currentUsercan be null while Auth initializes), checked 2 Nov 2026. - Firebase: Cloud Firestore quickstart (test mode and production mode), checked 2 Nov 2026.
- firebase-js-sdk source: util/error.ts and util/async_observer.ts (
toString()format, listener without an error callback), and issue #7160 (the message text, no document path), checked 2 Nov 2026.
Related errors
- Supabase row-level security violation:
new row violates row-level security policy for table - Node.js unhandled promise rejection:
This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch() - Cannot read properties of undefined:
TypeError: Cannot read properties of undefined (reading 'map') - Supabase Auth session missing:
AuthSessionMissingError: Auth session missing! - supabaseUrl is required:
supabaseUrl is required.
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.