Firebase Function 'PERMISSION_DENIED' on Firestore: Fix

Your Cloud Function deploys fine but fails with 7 PERMISSION_DENIED when it touches Firestore. Check the emulator, the service account and the auth context.

"Error: 7 PERMISSION_DENIED: Missing or insufficient permissions" in a Cloud Function usually has one of four causes: the function runs as a service account without Firestore access, you are using the client SDK so security rules apply, the callable function does not get a valid auth token, or your local emulator setup hid a rule that production enforces. Open the function logs, find which service account it runs as, and check its IAM roles first.

This error is confusing because the deploy succeeds and the code worked on your machine. Then a real user triggers the function and nothing is written. Below is the order to check, from the most common cause to the least.

Step 1: Read the Actual Error and Where It Comes From

Open Cloud Logging for the function, or run:

firebase functions:log --only myFunction

Look at the full message. A gRPC error with 7 PERMISSION_DENIED on a server function points to IAM or rules. A permission-denied code that reaches the browser from a client call points to rules or auth. Knowing which one you have saves a lot of guessing.

Also note whether the function uses the Admin SDK (firebase-admin) or the client SDK (firebase/firestore). The Admin SDK normally bypasses Firestore security rules and relies on IAM instead. The client SDK is subject to rules. Many reports in the Firebase talk group come from mixing the two.

Step 2: Check the Service Account and Its Roles

If you use the Admin SDK and see this error, suspect IAM. A firebase-functions GitHub issue tracks many reports of functions that deploy fine but fail on Firestore writes. The details:

  • 1st gen functions run as the App Engine default service account, PROJECT_ID@appspot.gserviceaccount.com.
  • 2nd gen functions run as the Compute Engine default service account, PROJECT_NUMBER-compute@developer.gserviceaccount.com.
  • These accounts normally get the Editor role automatically, which allows Firestore access. An organization policy can block that automatic grant. The issue quotes a Google note saying the constraint is enforced by default for organizations created after May 3, 2024.
  • One reporter found that a recommendation to remove unused permissions took away the Editor role and broke the function. Reverting restored access.

In the Google Cloud console, go to IAM and look up the account your function runs as. Then check the roles. The Cloud Run or Cloud Functions page for the function also shows its service account on the details tab.

Step 3: Give the Function the Smallest Role It Needs

The maintainer's suggested fixes in that issue are two. Either grant the Editor role to the default account, or create a custom service account with only the roles the function needs.

The custom account is the better choice. Editor is broad. For a function that reads and writes Firestore, roles/datastore.user is enough.

gcloud iam service-accounts create fn-firestore --display-name "Functions Firestore access"

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:fn-firestore@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/datastore.user"

Then attach it to the function. In 2nd gen with the Node.js SDK:

import { onRequest } from "firebase-functions/v2/https";

export const myFunction = onRequest(
  { serviceAccount: "fn-firestore@YOUR_PROJECT_ID.iam.gserviceaccount.com" },
  async (req, res) => {
    // Admin SDK calls now use this account
    res.send("ok");
  }
);

Redeploy and wait a few minutes. IAM changes are not always instant.

Step 4: Make Sure You Initialise the Admin SDK Correctly

A common mistake is importing the client SDK inside a function by accident. Auto-import in an editor makes this easy.

// Right for server code
import { initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";

initializeApp();
const db = getFirestore();

If you import from firebase/firestore in a function, the call has no signed-in user, so your rules reject it. Search your functions folder for from "firebase/ and replace any hit with the Admin equivalent.

Step 5: Check the Auth Context on Callable Functions

If the failing function is a callable (onCall), the problem may be the token, not IAM. A callable only sees request.auth when the client sends a valid Firebase ID token.

import { onCall, HttpsError } from "firebase-functions/v2/https";

export const saveProfile = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError("unauthenticated", "Sign in first.");
  }
  // request.auth.uid is safe to use here
});

Return unauthenticated yourself so the client gets a clear message. Then confirm the client is signed in before it calls the function, and that it uses the same Firebase project as the function. A mismatch between a dev and a prod project is a quiet cause. For a similar token problem on the auth side, see our guide on JWT expired errors in production.

Step 6: Rule Out an Emulator That Hid the Problem

If it works locally and fails in production, the emulator may not be the whole picture. In one Firebase talk thread, a developer saw permission errors despite open rules, because the app was actually talking to production, where the rules were stricter. Check that your client and function both point to the emulators when you test, and that production rules are what you expect.

Use the Rules Playground in the Firebase console to test a specific read or write against your deployed rules. It tells you which rule denies the request. Our post on Supabase row-level security errors covers the same "empty or denied in production" pattern for a different database.

Step 7: Alert on Function Errors Before Users Do

A function that fails on every Firestore write often looks healthy in the console: it is deployed and it runs. Only the logs show the error. Set a log-based alert on PERMISSION_DENIED in Cloud Logging, or watch your error rate after each deploy. Gen 2 function changes that move the service account can break things silently, so the first check after any deploy is whether writes still succeed.

How Relia Helps

This kind of failure is easy to miss, because the deploy looks fine and the symptom is a missing record. Relia watches the users, APIs, database and servers of a live product and joins related signals, such as a deploy and a failing request, into one incident explained in plain words, with who was affected. After a deploy, it checks whether a fix worked. Supabase, Firebase and edge functions are supported. For Node.js and browser apps, setup is npx relia-sdk@latest init. There is a Free plan and a live demo at app.tryrelia.com/demo.

FAQ

Why does my Firebase function get PERMISSION_DENIED after deploying?

Most often the function's service account lacks Firestore access, because an organization policy blocked the automatic Editor grant or someone removed the role. Check IAM for the account the function runs as.

Do Firestore security rules apply to Cloud Functions?

Not to the Admin SDK, which uses IAM. They do apply if you use the client SDK inside the function.

What service account do 2nd gen Cloud Functions use?

The Compute Engine default service account, PROJECT_NUMBER-compute@developer.gserviceaccount.com. 1st gen uses the App Engine default account.

How long do IAM changes take to apply?

Reporters in the GitHub issue note it can take a few minutes. Redeploy if it still fails after that.

Sources

All guides