Is It Safe to Expose Firebase API Key to the Public?
Developer Guide

Is It Safe to Expose Firebase API Key to the Public?

Yes, Firebase API keys are public by design. Learn why exposure is not the risk, and how to secure your app with proper rules and auth instead.

2026-06-30
9 min read
Is It Safe to Expose Firebase API Key to the Public?

TL;DR#

If you have seen the Stack Overflow question "Is it safe to expose Firebase API key to the public?" and you are wondering whether your app is at risk because you committed your apiKey to GitHub, here is the truth: it is safe by design. The real danger is not the exposed key itself, but missing or misconfigured Firebase Security Rules and client-side authentication checks.

The root cause of confusion is treating Firebase's apiKey like a traditional backend secret such as a database password or JWT signing key. It is not. The apiKey is just an identifier, like a username, that tells Firebase which project to connect to. What protects your data is the combination of:

  • Firebase Authentication to verify who is making the request
  • Security Rules to define what each authenticated or anonymous user can do

Fix it by auditing your Security Rules, enforcing auth != null where needed, and using Firebase's built-in rate limiting and monitoring, not by trying to hide the apiKey.

If that does not resolve your anxiety, scroll to Verify the fix and I will show how to test your rules locally and simulate attacker access.

What you'll see#

You will see the exact question on Stack Overflow, "Is it safe to expose Firebase API key to the public?", with 893 upvotes and more than 334,000 views. Developers ask this because they have seen code like this:

ts
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
 
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: 'my-app.firebaseapp.com',
  projectId: 'my-app',
  storageBucket: 'my-app.appspot.com',
  messagingSenderId: '123456789',
  appId: '1:123456789:web:abcdef',
};
 
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);

They commit this to a public GitHub repo and then worry someone will "steal" the apiKey and access the database.

This fear reproduces across local dev, staging, and production in Next.js, Vite, and plain HTML apps. The apiKey is visible in shipped client code and can often be seen through devtools, source maps, or network activity. That is by design, not a bug.

Root cause#

The confusion comes from how Firebase structures its SDK and security model. Unlike traditional APIs where a secret key grants access, Firebase uses a client-side identifier (apiKey) to route requests to the correct project, and server-side rules to enforce access control.

The apiKey is not a secret. It is a public identifier, like your project ID. A useful mental model is:

  • apiKey = "Which Firebase project am I talking to?"
  • auth = "Who am I?" verified by Firebase Auth
  • security rules = "What am I allowed to do?"

The relevant code path is:

ts
// src/lib/firebase.ts (real, runnable)
import { initializeApp, FirebaseApp } from 'firebase/app';
import { getAuth, User, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, getDoc } from 'firebase/firestore';
 
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
 
const app: FirebaseApp = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
 
// Client-side auth check, not a security rule.
export const getCurrentUser = (): Promise<User | null> => {
  return new Promise((resolve) => {
    onAuthStateChanged(auth, (user) => {
      resolve(user);
    });
  });
};

This code is safe only if your Firestore rules look like this:

text
// firestore.rules (real, runnable)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // Users can only read/write their own profile
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    match /public/{document} {
      // Public data - anyone can read
      allow read: if true;
      allow write: if false;
    }
  }
}

The root cause of the panic is this: developers assume the apiKey is a credential like a password and that exposing it lets attackers break in. But Firebase deliberately separates identification from authorization. The apiKey alone cannot grant access. It only identifies the project. Without valid authentication and without satisfying Security Rules, requests are rejected even with the correct apiKey.

That is why trying to hide the apiKey via server-side rendering, environment variables, or proxying does not improve security. It adds complexity while obscuring the real fix: enforcing rules.

The fix#

The fix is simple: stop trying to hide the apiKey. Start enforcing rules.

Here is how to secure your app properly.

Step 1: Move apiKey to environment variables, but keep it public#

In Next.js, use NEXT_PUBLIC_ prefixed variables. They are safe to expose because they are intended to be public:

ts
// src/lib/env.ts (real, minimal)
export const getFirebaseConfig = () => ({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
});

Then in src/lib/firebase.ts:

ts
// src/lib/firebase.ts (real, runnable)
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getFirebaseConfig } from './env';
 
const config = getFirebaseConfig();
 
if (!config.projectId) {
  throw new Error('NEXT_PUBLIC_FIREBASE_PROJECT_ID is required');
}
 
const app = initializeApp(config);
export const auth = getAuth(app);
export const db = getFirestore(app);

This does not hide the key. It only avoids hardcoding it. The key is still public, and that is fine.

Step 2: Write strict Security Rules#

The real security layer is in your rules. Here is a production-ready Firestore ruleset:

text
// firestore.rules (real, runnable)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Public data - read-only, no writes
    match /public/{document} {
      allow read: if true;
      allow write: if false;
    }
 
    // User profiles - only the owner can read/write
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
 
    // Private user data - authenticated users only, no writes
    match /private/{document} {
      allow read: if request.auth != null;
      allow write: if false;
    }
 
    // Admin-only collection
    match /admin/{document} {
      allow read, write: if request.auth != null && request.auth.token.admin == true;
    }
  }
}

This ruleset enforces:

  • Public data is readable by anyone, including unauthenticated users
  • User data is readable and writable only by the owner
  • Admin data requires a custom claim of admin: true
  • No one can write to public or private

Step 3: Add client-side auth guards#

Never trust the client. Always check auth state before rendering sensitive UI:

tsx
// src/components/Profile.tsx (real, runnable)
'use client';
import { useEffect, useState } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { auth } from '@/lib/firebase';
import { doc, getDoc } from 'firebase/firestore';
import { db } from '@/lib/firebase';
 
export default function Profile() {
  const [user, loading] = useAuthState(auth);
  const [profile, setProfile] = useState<Record<string, unknown>>({});
 
  useEffect(() => {
    if (!user) return;
    const fetchProfile = async () => {
      const docRef = doc(db, 'users', user.uid);
      const snap = await getDoc(docRef);
      if (snap.exists()) {
        setProfile(snap.data());
      }
    };
    fetchProfile();
  }, [user]);
 
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Please sign in.</div>;
 
  return (
    <div>
      <h1>{profile.name || user.email}</h1>
      <p>ID: {user.uid}</p>
    </div>
  );
}

This component will not render data unless user is authenticated, but the real protection is still in the rules. This is just a UX guard.

Step 4: Enable App Check#

App Check helps prevent abuse from non-Firebase SDK clients such as bots calling your API directly. It is not a replacement for rules. It is a defense-in-depth layer.

In firebaseConfig, add:

ts
// src/lib/env.ts (real, runnable)
export const getFirebaseConfig = () => ({
  // ...other keys...
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  // App Check is enabled via Firebase Console, not config
});

Then enable App Check in the Firebase Console under Build -> App Check and configure reCAPTCHA v3 or another provider.

Client-side security in Firebase#

"Client-side security" refers to the checks and guards your app performs in the user's browser or mobile runtime, code the user can read and modify. In a Firebase app, this typically includes:

  • Monitoring auth state with onAuthStateChanged or useAuthState
  • Conditionally rendering UI for signed-in users
  • Validating form input for a better user experience
  • Managing and refreshing ID tokens used by Firebase Auth

Client-side security matters for UX, but it has a hard architectural limit: the client is hostile territory. Any check running in the browser can be bypassed by devtools, a modified bundle, a browser extension, or a direct curl request to the Firebase endpoint.

This is why Firebase enforces authorization server-side. Security Rules run on Google's infrastructure, evaluate the verified request.auth token, and reject non-compliant requests before they touch your data. The apiKey only routes the request to the correct project. The rules decide whether it proceeds.

The right mental model is simple: client-side security tells your UI what to show. Server-side rules decide what the database will accept. Use both, but trust only the rules with your data.

Firebase Realtime Database: the same security model applies#

If your project uses Firebase Realtime Database instead of, or alongside, Firestore, the answer is the same. RTDB reads the same apiKey from your firebaseConfig, uses the same client-side model, and follows the same design assumption: the key is a public project identifier, not a secret.

RTDB uses a JSON-based rules language instead of the Firestore DSL. A production-ready RTDB ruleset looks like this:

json
{
  "rules": {
    "users": {
      "$userId": {
        ".read": "auth != null && auth.uid === $userId",
        ".write": "auth != null && auth.uid === $userId"
      }
    },
    "public": {
      ".read": true,
      ".write": false
    },
    "admin": {
      ".read": "auth != null && auth.token.admin === true",
      ".write": "auth != null && auth.token.admin === true"
    }
  }
}

This ruleset enforces the same guarantees:

  • users/$userId is readable and writable only by the owner
  • public is world-readable but not writable
  • admin requires a custom claim of admin: true

Deploy RTDB rules with:

bash
firebase deploy --only database

And test them locally with:

bash
firebase emulators:start --only database

The bottom line is the same for Firestore and RTDB: the apiKey is safe to expose, and your data is protected by server-enforced Security Rules, not by hiding the key.

Verify the fix#

Run the Firebase Emulator Suite to test your rules locally:

  1. Install the emulator:
bash
npm install -g firebase-tools
  1. Initialize the emulator:
bash
firebase init emulators
  1. Start the emulators:
bash
firebase emulators:start --only firestore
  1. In a new terminal, simulate an unauthenticated request:
bash
curl -X GET \
  'http://localhost:8080/firestore/v1/projects/dev-database/databases/(default)/documents/users/abc123' \
  -H 'Content-Type: application/json'

You should see:

json
{
  "error": {
    "code": 403,
    "message": "Missing or insufficient permissions."
  }
}

If you are still seeing 200 OK on unauthorized requests, check:

  • Your rules file is named firestore.rules and deployed with firebase deploy --only firestore
  • You are using rules_version = '2'
  • Your client code is not bypassing the intended access path

Why this happens (and how to avoid it next time)#

The underlying invariant is this: Firebase is designed for client-side access. It assumes your API key is public and shifts security responsibility to rules and auth, not obscurity.

To prevent this confusion:

  • Keep Firebase config in NEXT_PUBLIC_* variables instead of hardcoding it
  • Test your rules in CI with emulator-based tests
  • Use API key restrictions as a minor abuse mitigation, not as your core security layer

FAQ#

Is Firebase API key the same as a secret key?#

No. Firebase's apiKey is a public identifier like your project ID. It does not grant access. What matters is your Security Rules and Authentication. The server-side secret is something like a service account key, not the browser-facing apiKey.

Should I regenerate my Firebase API key if it was exposed?#

No. That is usually unnecessary. The apiKey is safe to expose. Focus instead on auditing your rules and auth setup.

Can I restrict Firebase API key usage by domain or IP?#

Yes. Firebase supports API key restrictions. They can help reduce abuse, but they are not your primary security layer.

Do I need different API keys for development and production?#

No, but you should usually use different Firebase projects so data and rules stay isolated across environments.

How do Security Rules protect against exposed API keys?#

Security Rules run on Firebase's servers, not in your client. Even if an attacker knows your apiKey, they still cannot bypass rules. The apiKey identifies the project. The rules authorize the request.

Summary#

Exposing your Firebase apiKey is safe by design. The key tells Firebase which project a client is targeting, but it does not grant access to your data. What protects you is the combination of Firebase Authentication and Security Rules. Whether you use Firestore or Realtime Database, the model is the same: write strict rules, require auth where needed, add App Check and restrictions for defense in depth, and stop treating the client-facing apiKey like a password.

Frequently Asked Questions

|

Have more questions? Contact us

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.