Supabase

auth/invalid-api-key still throws without a real apiKey

The rejection happens one function call earlier than most people expect, before any server, project or emulator is ever reached.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
7 min read

TL;DR#

getAuth(app) throws auth/invalid-api-key even when the app only ever talks to the local Auth emulator, because the check that rejects a missing apiKey runs synchronously inside getAuth()/initializeAuth(), before connectAuthEmulator() is called and before any network request exists. The SDK does not special-case the emulator. Pass any non-empty string without a colon as apiKey — it is never sent anywhere when the emulator is in use. Reported in firebase/firebase-js-sdk#6776.

The verbatim error, from the issue:

text
FirebaseError: Firebase: Error (auth/invalid-api-key).
 
       7 | });
       8 | export const firestore = getFirestore(app);
    >  9 | export const auth = getAuth(app);
         |                            ^
      10 |
      11 | connectFirestoreEmulator(firestore, 'localhost', 8080);
      12 | connectAuthEmulator(auth, 'http://localhost:8081');

Why it happens#

Firebase Auth's own registration code runs an assertion the first time any component (getAuth, initializeAuth) constructs the Auth instance from app.options. In the current main branch of firebase-js-sdk (commit d7bb0e1), packages/auth/src/core/auth/register.ts reads:

ts
const { apiKey, authDomain } = app.options;
 
_assert(
  apiKey && !apiKey.includes(':'),
  AuthErrorCode.INVALID_API_KEY,
  { appName: app.name }
);

This runs before AuthImpl is constructed and before connectAuthEmulator() can redirect any request to localhost. There is no code path that skips it for a demo--prefixed projectId or for an emulator host. The reporter's CodeSandbox reproduction, linked from issue #6776, shows the same symptom on a demo-test project with no apiKey set.

A pull request, #7278 "Removes assertion that API key is nonempty", proposes dropping the non-empty check specifically. As of this writing that PR is open, not merged — treat it as a proposal, not a fix shipped in any released version.

Reproduced#

Against the real SDK, in a throwaway Node 20 container, with firebase 13.0.0 installed from the npm registry (16 October 2026):

text
firebase package version installed: 13.0.0
--- Test 1 - no apiKey, demo- projectId ---
Test 1 - no apiKey, demo- projectId threw: FirebaseError: Firebase: Error (auth/invalid-api-key).
--- Test 2 - apiKey undefined explicitly ---
Test 2 - apiKey undefined explicitly threw: FirebaseError: Firebase: Error (auth/invalid-api-key).
--- Test 3 - apiKey empty string ---
Test 3 - apiKey empty string threw: FirebaseError: Firebase: Error (auth/invalid-api-key).
--- Test 4 - apiKey is string 'undefined' (template string bug) ---
Test 4 - apiKey is string 'undefined' (template string bug): no throw, getAuth + connectAuthEmulator succeeded
--- Test 5 - apiKey dummy string works ---
Test 5 - apiKey dummy string works: no throw, getAuth + connectAuthEmulator succeeded
--- Test 6 - apiKey contains a colon (negative control) ---
Test 6 - apiKey contains a colon (negative control) threw: FirebaseError: Firebase: Error (auth/invalid-api-key).

Four things follow directly from this run:

  • The error reproduces on the current SDK (13.0.0), two and a half years after the issue was filed — it is not an edge case that has since been fixed.
  • Setting apiKey to undefined explicitly, or to an empty string, is exactly as bad as omitting it: all three throw.
  • A demo--prefixed projectId (the pattern Firebase documents for emulator-only apps) changes nothing here — getAuth() never reaches the part of the code that would read it.
  • `${undefined}` — the JavaScript string "undefined" produced by a template literal around an unset variable — passes the check. This is the accidental "fix" one commenter on the issue describes getting from a template string: it is not a deliberate workaround, it is a non-empty string without a colon that happens to satisfy the assertion by accident.
  • A colon in the value still throws (the assertion's second condition), which is only a concern if a secret-manager placeholder or a copied connection string happens to land in apiKey.

This part of the behaviour is reproduced locally and directly from source. What is not reproduced here: whether the Auth emulator accepts sign-in calls with a dummy apiKey once connectAuthEmulator() has run. One commenter on the issue reports it works with process.env.FIREBASE_API_KEY || "PUT_IN_A_DUMMY_API_KIEY"; that is the reporter's own account, not verified in this article beyond getAuth() itself completing without throwing.

Fix#

1. Give apiKey any non-empty value without a colon when only the emulator is used#

js
import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";
 
const app = initializeApp({
  projectId: "demo-my-project", // the demo- prefix is Firebase's documented emulator-only convention
  apiKey: process.env.FIREBASE_API_KEY || "emulator-only-dummy-key",
});
 
const auth = getAuth(app);
connectAuthEmulator(auth, "http://127.0.0.1:9099");

The value is never checked against a real Firebase project when connectAuthEmulator() runs first — the assertion only cares that the string is non-empty and contains no colon. Falling back to a literal keeps local development working without a .env file, while a real FIREBASE_API_KEY is still picked up in any environment where one is set (staging, CI against a real project).

2. Do not rely on a template-string accident#

If a codebase currently "works" because apiKey: `${process.env.FIREBASE_API_KEY}` silently becomes the literal string "undefined" when the variable is unset, replace it with an explicit fallback as in step 1. The template-string version passes the SDK's check for the wrong reason — it says nothing about whether the intended key was ever read — and it will keep "working" even after a real misconfiguration, which hides the next failure instead of surfacing it.

3. Keep the dummy value out of anywhere that reaches production#

apiKey guards nothing by itself in Firebase's model — Security Rules and App Check do (see the Firebase public API-key guide for why the key itself is not the secret). But an emulator-only dummy string is still not a real project identifier: initialising the production build with "emulator-only-dummy-key" would fail against Firebase's real auth servers, which do validate the key format and project association server-side. Gate the fallback on an explicit condition tied to how the app already decides it is running against the emulator, rather than a bare || that would also silently trigger in a deployed build that is simply missing its real env var.

Verify the fix#

Re-run the app with the emulator started (firebase emulators:start --only auth) and the dummy apiKey in place. getAuth(app) should return without throwing, and connectAuthEmulator()'s one-time console warning ("You are using the Auth Emulator...") should appear instead of the auth/invalid-api-key stack trace. That warning, observed in the reproduction above, is the SDK's own confirmation that it accepted the configuration and is routing to 127.0.0.1 rather than Firebase's servers.

Then exercise an actual emulator call — createUserWithEmailAndPassword, signInWithEmailAndPassword, or whatever the app's flow uses — against the running emulator. A successful getAuth() call only proves the assertion passed; it does not prove the emulator accepts every subsequent request, so confirm the real flow separately before trusting the fix in a test suite.

If the app instead throws Firebase: Error (auth/network-request-failed) after this fix, the emulator most likely is not running on the host and port passed to connectAuthEmulator() — check the emulator's logged 127.0.0.1:<port> against the URL in code, not localhost, which resolves differently inside some containers and CI runners.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Where this leads next#

Getting past auth/invalid-api-key only proves the client constructed an Auth instance — it says nothing about what the signed-in user is then allowed to do. The next error most reporters of this kind of emulator setup hit is Firestore rejecting an authenticated read or write as missing or insufficient permissions, usually because the emulator's Firestore rules were never deployed or a rule checks a field the request does not send; that error has seven distinct causes worth ruling out in order.

A separate build-time error sometimes follows once the dummy key is in place and the project moves from local testing to a production bundle: a webpack resolver failure reading Module not found: Default condition should be last one, caused by a package.json exports ordering bug in some Firebase releases rather than by anything in apiKey. That error has its own fixed version and detection method and is unrelated to the assertion described above.

Further reading: Fix Firebase 'Invalid site key or not loaded in api.js'. Also see Google sign-in fails on an iOS PWA saved to Home Screen. Also see linkWithRedirect fails with the Auth emulator: 3 causes. Also see useEmulator disableWarnings: TS2554 Expected 1 arguments. Also see auth/popup-closed-by-user fires 8 s late: cause and fix.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote