# Fix: FirebaseError: Missing or insufficient permissions.

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

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.

```ts
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.

```tsx
'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

```text
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.

```ts
// 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

```ts
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:

```ts
onSnapshot(
  q,
  (snap) => setPosts(snap.docs.map((d) => d.data())),
  (err) => {
    if (err.code === 'permission-denied') reportError(err);
  },
);
```

Install the browser SDK once:

```ts
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](https://firebase.google.com/docs/firestore/security/rules-conditions) (`request.auth`, rules are not filters, `request.resource.data`), checked 2 Nov 2026.
- [Firebase: Get started with Cloud Firestore Security Rules](https://firebase.google.com/docs/firestore/security/get-started) (server client libraries bypass rules), checked 2 Nov 2026.
- [Firebase: Manage users on the web](https://firebase.google.com/docs/auth/web/manage-users) (`onAuthStateChanged`, `currentUser` can be null while Auth initializes), checked 2 Nov 2026.
- [Firebase: Cloud Firestore quickstart](https://firebase.google.com/docs/firestore/quickstart) (test mode and production mode), checked 2 Nov 2026.
- [firebase-js-sdk source: util/error.ts](https://github.com/firebase/firebase-js-sdk/blob/main/packages/firestore/src/util/error.ts) and [util/async_observer.ts](https://github.com/firebase/firebase-js-sdk/blob/main/packages/firestore/src/util/async_observer.ts) (`toString()` format, listener without an error callback), and [issue #7160](https://github.com/firebase/firebase-js-sdk/issues/7160) (the message text, no document path), checked 2 Nov 2026.

## Related errors

- [Supabase row-level security violation](/guides/supabase-rls-violation/): `new row violates row-level security policy for table`
- [Node.js unhandled promise rejection](/guides/node-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](/guides/cannot-read-properties-of-undefined/): `TypeError: Cannot read properties of undefined (reading 'map')`
- [Supabase Auth session missing](/guides/supabase-auth-session-missing/): `AuthSessionMissingError: Auth session missing!`
- [supabaseUrl is required](/guides/supabase-url-is-required/): `supabaseUrl is required.`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
