Where the trust boundary is
The config object in your front end, including the API key, is public by design. Firebase's own documentation says these keys only identify the project and app, and that authorization is handled by Google Cloud IAM, Security Rules and App Check. So the question is never whether the key is visible. It is whether a request carrying that key is allowed by your rules.
Rules run on Google's servers for every client request. They do not apply to the Admin SDK, which runs with elevated privileges on your server, so anything you do there needs its own authorization checks.
The checklist
| Area | What to verify | How to test on your own project | Pass condition |
|---|---|---|---|
| Firestore rules | No allow read, write: if true; access tied to request.auth.uid | Read firestore.rules; run the outside request in Step 2 | Unauthenticated request returns a permission error |
| Realtime Database rules | No true at a parent path that grants access to private children | Read database.rules.json; request the .json endpoint logged out | Permission denied on private paths |
| Storage rules | Paths keyed to user id, write requires auth, sizes and types limited | Read storage.rules; list the bucket without credentials | Listing and private reads are denied |
| App Check | Enforcement is on for each product you use | Check the App Check console for each service's enforcement state | Enforced, not just monitoring |
| Auth | Only the sign-in providers you intend are enabled; authorized domains are yours | Review Authentication settings | No unused providers; no stray domains |
| API keys | Key restricted to the APIs and apps you use; separate keys for non-Firebase APIs | Open Credentials in Google Cloud Console and read each key's restrictions | Application and API restrictions set |
Step 1: Read the rules like an attacker
Open your rules files or the Rules tab for each product. Firebase's documentation calls out the patterns that expose data: allow read, write: if true, and checks that only require request.auth != null, which lets any signed-in user touch everything. Test mode rules that expire also break apps abruptly, and blanket denials block clients, so neither is a fix.
Tie every rule to ownership or a role. Custom claims set with the Admin SDK are readable as request.auth.token, which avoids an extra document read for role checks.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
allow read, delete: if request.auth != null
&& request.auth.uid == resource.data.author_uid;
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.author_uid;
}
}
}Step 2: Probe from outside
Run these against your own project with no credentials. Each should return a permission error. A JSON body with documents, keys or object names means the rules are open.
curl -s "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/notes?pageSize=1"
curl -s "https://PROJECT_ID-default-rtdb.firebaseio.com/.json?shallow=true"
curl -s "https://firebasestorage.googleapis.com/v0/b/BUCKET/o?maxResults=1"Step 3: Realtime Database cascading
In Realtime Database, a child rule cannot take away access granted by a parent. If /users allows reads, setting read to false on a child does nothing. Grant narrowly at the deepest path instead, and keep private data under a path keyed by user id.
{
"rules": {
"users": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid"
}
}
}
}Step 4: Storage rules
Match uploads to a path containing the user id and require a signed-in user to write. Public read is fine for avatars and wrong for documents, so split them by path. Add limits on content type and size for uploads, since rules can inspect request.resource.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{fileName} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null && request.auth.uid == userId;
}
}
}Step 5: Test in the emulator, then deploy
Firebase recommends the Local Emulator Suite and the @firebase/rules-unit-testing library for validating rules before production. Write at least three tests per collection: owner allowed, other user denied, signed-out denied. Run them with emulators:exec so they can run in CI.
firebase emulators:exec --only firestore "npm test"
firebase deploy --only firestore:rulesStep 6: App Check, Auth and key restrictions
App Check attests that requests come from your genuine app, using providers such as Play Integrity, App Attest or reCAPTCHA Enterprise. It supports Firestore, Realtime Database, Storage and callable Cloud Functions. Firebase is explicit that it complements Authentication and Rules rather than replacing them: it protects your backend from abuse, not your users' data from other users. Turn on enforcement once your app sends tokens.
In Authentication, disable providers you do not use and check the authorized domains list. In Google Cloud Console, open Credentials and confirm each key has API restrictions and application restrictions (bundle ID, package name or referrer). Use a separate key for any non-Firebase Google API.
Common mistakes
- Leaving the initial test-mode or if true rule in place.
- Believing that a hidden API key is what protects the database.
- Checking only request.auth != null, so any account can read everyone's data.
- Setting a Realtime Database parent rule to true and expecting child rules to override it.
- Turning on App Check in monitoring mode and never enforcing it.
- Trusting client-supplied fields such as role or author_uid without comparing them to request.auth.
Keep it working
Keep rules in version control, run the emulator tests on every change, and repeat the outside requests after each deploy. When an assistant edits rules to fix a permission error, read the diff before deploying.
Frequently asked questions
Is my Firebase API key a secret?
No. Firebase documents these keys as identifiers for your project and app, safe to include in client code when restricted. Protection comes from Security Rules, App Check and IAM. Restrict the key anyway, and never reuse it for unrelated paid APIs.
Does App Check replace security rules?
No. App Check verifies the app and device making a request, while rules decide what a user may read or write. You need both. App Check reduces abuse of your quota and backend, rules protect the data.
Do rules apply to my server code?
Not to the Admin SDK, which bypasses them. If your server writes on behalf of users, verify the user's ID token and check ownership in your own code before touching data.
How do I know my rules work?
Write emulator tests for owner, other user and signed-out cases, and run the unauthenticated requests in this checklist against production. Both should agree that private data is denied.