Fix: Firebase Permission Denied Error
Quick Answer
How to fix the Firebase 'permission denied' or 'Missing or insufficient permissions' error in Firestore and Realtime Database. Covers security rules, authentication state, custom claims, Admin SDK, rule simulators, time-based rules, and document-level permissions.
The Error
You read or write data in Firebase, and the operation fails with one of these messages:
Firestore:
FirebaseError: Missing or insufficient permissions.Realtime Database:
Error: permission_denied at /path: Client doesn't have permission to access the desired data.Or in the Firebase console Rules Playground, your simulated read or write returns deny.
The data exists. Your code looks correct. But Firebase refuses to let you touch it.
Why This Happens
Every Firebase database request is checked against security rules before it reaches your data. If the rules do not explicitly grant access for the operation you are attempting, Firebase denies it. There is no implicit allow — the default is deny.
The most common causes:
- Default locked rules — New Firestore projects ship with
allow read, write: if false;. New Realtime Database projects ship with".read": false, ".write": false. Neither allows anything. - Unauthenticated requests — Your rules require
request.auth != null, but the user is not signed in, or the auth state has not loaded yet when you make the request. - Wrong rule path — The rule covers
/users/{userId}but your code reads/Users/{userId}(case mismatch) or a nested subcollection that has no matching rule. - Client SDK vs Admin SDK confusion — You use the client SDK on a server expecting it to bypass rules, but only the Admin SDK does that.
- Custom claims not set or not refreshed — Your rules check
request.auth.token.admin == true, but the claim was never set or the user has a stale ID token. - Document-level restrictions — The rule checks
resource.data.ownerId == request.auth.uid, but the document’sownerIdfield does not match. - Time-based rules expired — You used a temporary
allow read, write: if request.time < timestamp.date(2026, 3, 1);during development and the date has passed. - Realtime Database cascading rules — A parent node grants access, but you expect a child node rule to restrict it. Realtime Database rules cascade downward — once granted, child rules cannot revoke access.
Fix 1: Update Your Security Rules to Allow Access
Start by checking what your rules actually say. Open the Firebase Console > Firestore Database > Rules (or Realtime Database > Rules).
Firestore — allow authenticated reads and writes:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if request.auth != null;
allow write: if request.auth != null;
}
}
}Realtime Database — allow authenticated reads and writes:
{
"rules": {
"posts": {
".read": "auth !== null",
".write": "auth !== null"
}
}
}Warning: Never use allow read, write: if true; or ".read": true, ".write": true in production. These rules let anyone read and write all your data, including unauthenticated users and attackers. Use them only for quick local testing, and set a reminder to lock them back down.
If you are early in development and just need things working temporarily, Firestore supports time-limited open rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 4, 1);
}
}
}This auto-locks on the specified date. But do not ship this to production.
After updating rules, click Publish. Firestore rule changes take effect within about 60 seconds. Realtime Database rule changes are immediate.
Fix 2: Ensure the User Is Authenticated Before Querying
A common pattern is firing a database query at page load before Firebase Auth has resolved the user’s sign-in state. The query runs while auth.currentUser is still null, so rules that require request.auth != null reject it.
Wait for the auth state to resolve first:
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) {
// Auth is ready — safe to query
const snap = await getDoc(doc(db, "users", user.uid));
console.log(snap.data());
} else {
console.log("User not signed in");
}
});In React, this often means gating your data fetch behind a loading state. If you have encountered issues with async state in React, the patterns in the guide on fixing TypeError: Cannot read properties of undefined apply here too — you need to handle the case where data is not yet available.
Pro Tip: If you use Firebase Auth with a framework like Next.js or Nuxt, the server-side render has no client-side auth state. You must pass the user’s ID token to the server and verify it with the Admin SDK, or use a session cookie. Client-side
onAuthStateChangedonly works in the browser.
Fix 3: Match Your Rule Paths to Your Query Paths
Firestore rules are path-specific. A rule on /users/{userId} does not cover /users/{userId}/orders/{orderId}. Subcollections need their own rules.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
// Subcollection needs its own rule
match /orders/{orderId} {
allow read, write: if request.auth.uid == userId;
}
}
}
}If you want a single rule to cover a document and all its subcollections, use the recursive wildcard:
match /users/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}For Realtime Database, paths are case-sensitive. If your code writes to /Users/abc123 but your rule covers /users/$uid, the rule does not match. Double-check capitalization in both your code and rules.
Fix 4: Use the Admin SDK for Server-Side Operations
The Firebase client SDK (the one you install with npm install firebase) always enforces security rules. If you are running code on a trusted server or Cloud Function that needs unrestricted access, use the Admin SDK instead.
const admin = require("firebase-admin");
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
const db = admin.firestore();
// This bypasses security rules entirely
const snap = await db.collection("users").doc("abc123").get();Common Mistake: Some developers import the client SDK (firebase/firestore) in their Cloud Functions or Express server, then wonder why permission errors occur even though they believe server code should bypass rules. The client SDK never bypasses rules, regardless of where it runs. You must use firebase-admin for rule-free access.
Install it separately:
npm install firebase-adminThe Admin SDK authenticates with a service account, not a user account. It does not use security rules at all — every read and write succeeds (assuming the data path exists). This is why you should never expose Admin SDK credentials to the client.
Fix 5: Set and Refresh Custom Claims
If your rules check custom claims like request.auth.token.admin, you need to set those claims using the Admin SDK and make sure the client has a fresh ID token.
Set the claim (server-side):
const admin = require("firebase-admin");
await admin.auth().setCustomUserClaims("USER_UID_HERE", {
admin: true,
});Force a token refresh (client-side):
import { getAuth } from "firebase/auth";
const auth = getAuth();
// Force refresh to pick up new claims
await auth.currentUser.getIdToken(true);Custom claims are embedded in the ID token. If you set a claim and the user already has a token, the old token (without the claim) remains valid until it expires (1 hour). Calling getIdToken(true) forces a refresh immediately.
Check claims in your rules like this:
match /admin/{document=**} {
allow read, write: if request.auth.token.admin == true;
}If environment variables are involved in your server-side claim-setting code, make sure they load correctly. Misconfigured env vars are a frequent source of silent failures — see the guide on fixing process.env undefined for common pitfalls.
Fix 6: Use the Rules Playground and Emulator to Debug
Guessing which rule is denying your request wastes time. Firebase provides two tools for testing rules directly.
Rules Playground (Firebase Console)
- Go to Firestore Database > Rules in the Firebase Console.
- Click the Rules Playground tab (or the play button icon).
- Select the operation type (get, list, create, update, delete).
- Enter the document path (e.g.,
/users/abc123). - Toggle Authentication on and provide a mock
authobject withuid,email, and custom claims. - Click Run. The playground shows whether the operation is allowed or denied, and highlights which rule line made the decision.
Firebase Emulator Suite
For more thorough testing, run the local emulator:
firebase emulators:start --only firestoreThen write unit tests against your rules:
const { initializeTestEnvironment, assertSucceeds, assertFails } =
require("@firebase/rules-unit-testing");
const testEnv = await initializeTestEnvironment({
projectId: "my-project",
firestore: {
rules: fs.readFileSync("firestore.rules", "utf8"),
},
});
// Test authenticated user reading their own doc
const alice = testEnv.authenticatedContext("alice");
await assertSucceeds(
alice.firestore().collection("users").doc("alice").get()
);
// Test unauthenticated user being denied
const unauth = testEnv.unauthenticatedContext();
await assertFails(
unauth.firestore().collection("users").doc("alice").get()
);This catches rule issues before you deploy. Run it as part of your CI pipeline so rules regressions do not reach production.
Fix 7: Fix Document-Level Permission Checks
Rules that reference resource.data inspect the existing document in the database. If the document does not exist yet (e.g., you are creating it), resource.data is null, and the rule fails.
Use request.resource.data for incoming data on writes, and resource.data for existing data on reads and updates:
match /posts/{postId} {
// Anyone can read published posts
allow read: if resource.data.published == true;
// Only the owner can update their own posts
allow update: if request.auth.uid == resource.data.ownerId;
// On create, the ownerId must match the authenticated user
allow create: if request.auth.uid == request.resource.data.ownerId;
}A subtle mistake: checking resource.data.ownerId on a create operation. The document does not exist yet during creation, so resource.data is null and the rule denies. Use request.resource.data for create operations.
Also, if your rule checks a field like resource.data.status, but some documents were created before that field existed, those documents will fail the rule check. You can guard against this:
allow read: if !("status" in resource.data) || resource.data.status == "active";This is similar in spirit to defensive coding against undefined properties in JavaScript — the same kind of issue covered in fixing Cannot read properties of undefined applies at the database rules level.
Fix 8: Handle Realtime Database Rule Cascading and Validation
Realtime Database rules work differently from Firestore rules in one critical way: access cascades downward. If a parent node grants read access, every child node is readable regardless of what child-level rules say.
{
"rules": {
"users": {
".read": true,
"$userId": {
"private": {
".read": false
}
}
}
}
}In this example, users/$userId/private is still readable because users has .read: true. The child .read: false does not override the parent grant.
To fix this, move the read permission down to the level you actually want:
{
"rules": {
"users": {
"$userId": {
"public": {
".read": true
},
"private": {
".read": "auth !== null && auth.uid === $userId"
}
}
}
}
}Validate rules (.validate) are the exception — they do not cascade and only apply to the exact node where they are defined. Use .validate for data shape enforcement:
{
"rules": {
"posts": {
"$postId": {
".write": "auth !== null",
".validate": "newData.hasChildren(['title', 'body', 'authorId'])",
"title": {
".validate": "newData.isString() && newData.val().length <= 200"
}
}
}
}
}If you are working with server infrastructure and hitting similar access issues with other services, the troubleshooting approach in fixing AWS AccessDeniedException follows a comparable pattern of checking policies, roles, and permission boundaries.
Still Not Working?
If you have tried the fixes above and still get permission denied:
Check for query constraints that violate rules. In Firestore, if your rule says allow read: if request.auth.uid == resource.data.ownerId, but you run a query like collection("posts").get() without a where("ownerId", "==", uid) clause, Firestore rejects the entire query. Firestore evaluates rules against the potential result set, not individual documents. Add a matching where clause so Firestore can prove every result would pass the rule.
Verify your Firebase project ID. If you have multiple Firebase projects (dev, staging, prod), you might be authenticated against one project but querying another. Check firebase use in your CLI and the config object in your app initialization.
Check for Firestore index requirements. Some compound queries require composite indexes. While this usually produces a different error (FAILED_PRECONDITION), in rare cases the error surfaces as a permission issue. Click the link in the error message to create the required index.
Look at Firebase Console > Authentication > Users. Confirm the user you are testing with actually exists, has a verified email (if your rules check request.auth.token.email_verified), and has the correct UID.
Check for CORS issues on REST API calls. If you are calling the Firestore REST API directly instead of using the SDK, CORS misconfigurations can mask the real error. The browser may show a generic network error instead of the actual permission denied response. See the guide on fixing CORS errors if you suspect this.
Inspect the full error object. Firebase errors include a code property. Check whether it is permission-denied, unauthenticated, or something else entirely:
try {
const snap = await getDoc(doc(db, "users", "abc123"));
} catch (error) {
console.error("Code:", error.code);
console.error("Message:", error.message);
}permission-denied means your rules blocked it. unauthenticated means there is no valid auth token at all — go back to Fix 2.
Review Cloud Firestore audit logs. Enable Data Access audit logs in Google Cloud Console under IAM & Admin > Audit Logs > Cloud Firestore. These logs show every denied request with the exact rule evaluation path, which is invaluable for debugging complex rule sets.
Test with a minimal rule set. Temporarily simplify your rules to allow read, write: if request.auth != null for the specific collection causing issues. If that works, gradually add your conditions back one at a time until you find the one that breaks. This is faster than staring at a complex rule file. If you are debugging server-side issues involving connection problems, the methodology in fixing ERR_CONNECTION_REFUSED can help you isolate whether the problem is network-level or application-level.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Warning: Failed prop type
How to fix the React 'Warning: Failed prop type' error. Covers wrong prop types, missing required props, children type issues, shape and oneOf PropTypes, migrating to TypeScript, default props, and third-party component mismatches.
Fix: Express Cannot GET /route (404 Not Found)
How to fix Express.js Cannot GET route 404 error caused by wrong route paths, missing middleware, route order issues, static files, and router mounting problems.
Fix: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
How to fix the JavaScript heap out of memory error by increasing Node.js memory limits, fixing memory leaks, and optimizing builds in webpack, Vite, and Docker.
Fix: TypeError: x is not a function
How to fix JavaScript TypeError is not a function caused by wrong variable types, missing imports, overwritten variables, incorrect method names, and callback issues.