Home

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.

FactDetail
ErrorFirebaseError: Missing or insufficient permissions.
Error codepermission-denied
Where you see itBrowser console, the rejected promise from getDoc / setDoc / getDocs, the error callback of onSnapshot
Usual causeThe request runs before sign-in resolves, or the rules do not cover that path or query
Usual fixQuery after onAuthStateChanged gives you a user, and write rules that match the path and the query
BugTape catches itYes, 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

  1. The query runs before Auth is ready. On page load, auth.currentUser is null until Firebase Auth has finished initializing. A query sent in that window has request.auth == null.
  2. The rules do not match the path. A rule on match /users/{userId} does not cover users/{userId}/orders/{orderId}. Subcollections need their own match block or a recursive wildcard.
  3. The query could return documents the user may not read. Rules are not filters. A query for the whole posts collection fails when the rule only allows resource.data.authorId == request.auth.uid, even if every stored post belongs to the user. Add the same condition to the query.
  4. A write fails a condition on the new data. request.resource.data holds the document as it would be after the write. A missing or wrong field, such as ownerId, fails the rule.
  5. 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.
  6. Production mode defaults. A database created in production mode denies all reads and writes from mobile and web clients until you add rules.
  7. The user signed out while a listener was open. An onSnapshot listener 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 no try/catch becomes 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 calls reportError.
  • 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

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.