Is my Firebase app secure?

Firebase security comes down to one thing, and it isn't the thing most people worry about. The config sitting in your HTML is fine. Your Security Rules are carrying the entire weight โ€” and the default ones don't.

1. Your Firebase config is not a leak

Every Firebase app ships a block like this to the browser, and every few weeks someone finds theirs and panics:

const firebaseConfig = {
  apiKey: "AIzaโ€ฆ",
  authDomain: "myapp.firebaseapp.com",
  projectId: "myapp",
  // โ€ฆ
}

That's supposed to be public. Despite the name, the Firebase apiKey isn't a secret โ€” it identifies your project to Google's servers, it doesn't authorise anything. Google publishes it in their own sample code. You cannot hide it, because the browser needs it to know which project to talk to.

So don't waste an afternoon moving it into an environment variable. It will end up in the bundle anyway, and it was never the problem.

But here's the part that follows from it, and it's the whole point of this page: if anyone can take your config, then anyone can talk to your database. Not through your app โ€” with their own script, from their own machine, bypassing your UI entirely. The only thing deciding what they get back is your Security Rules.

2. Test mode is a time bomb, and locked mode is a cliff

When you create a Firestore database, Firebase offers you two starting points. Test mode gives you rules that look roughly like this:

allow read, write: if request.time < timestamp.date(2026, 3, 1);

Read that carefully. Until that date, anyone at all can read and write everything. There is no authentication check in there. It's meant for the first week of a prototype, and Firebase makes it expire specifically so you can't forget about it forever.

Then the date passes, your app breaks completely, and you're debugging a production outage. The fastest thing that makes it work again is:

allow read, write: if true;   // never ship this

That's the single most common way a Firebase app ends up fully public โ€” not ignorance, but an outage fixed under pressure. If your rules contain if true or a date comparison, treat it as an open database.

3. request.auth != null is not authorisation

The usual second attempt looks responsible and isn't:

match /users/{userId} {
  allow read, write: if request.auth != null;   // any signed-in user
}

That says "anyone logged in", not "the person this data belongs to". Anyone who can sign up โ€” which, if you have public registration, is everyone โ€” can then read and overwrite every other user's document.

What you want is a comparison against the record's owner:

match /users/{userId} {
  allow read, write: if request.auth != null
                     && request.auth.uid == userId;
}

And for documents that store the owner in a field rather than the path:

match /projects/{projectId} {
  allow read:   if request.auth.uid == resource.data.ownerId;
  allow create: if request.auth.uid == request.resource.data.ownerId;
  allow update, delete: if request.auth.uid == resource.data.ownerId;
}

Note the two different objects: resource.data is the document as it exists now, request.resource.data is what's being written. Checking only the first on a create is a common hole โ€” there's no existing document, so the condition passes vacuously.

4. Rules don't cascade the way you expect (and the two databases differ)

This one catches people who did everything else right.

In Firestore, rules do not apply to subcollections unless you match them. A rule on /users/{userId} says nothing about /users/{userId}/notes/{noteId} โ€” that path is unmatched, so access is denied by default, which is safe. The danger is the fix people reach for:

match /users/{userId}/{document=**} {
  allow read, write: if request.auth.uid == userId;
}

That recursive wildcard is correct here, but it grants everything beneath that path โ€” so if you later add a subcollection that shouldn't follow the same rule, it silently inherits it.

Realtime Database works the opposite way. Rules cascade downward, and a grant at a parent path cannot be revoked deeper. If you allow read at the root, adding a stricter rule on a child does nothing โ€” the read is already permitted. So a single permissive rule high up quietly overrides everything below it.

5. Storage rules are a separate file

Locking down Firestore does nothing for Cloud Storage. They're different rule sets, edited in different places, and it's easy to secure your database carefully and leave every uploaded file world-readable โ€” or worse, world-writable, which turns your bucket into free hosting for whatever a stranger uploads and puts it on your bill.

Check them separately, and scope them to the owner the same way:

match /users/{userId}/{fileName} {
  allow read, write: if request.auth != null
                     && request.auth.uid == userId;
}

6. Rules aren't filters โ€” and that's why people loosen them

A subtlety worth knowing before it bites you: Firestore rules don't filter results. If you query a collection and the rules can't guarantee that every document the query could return is readable, the whole query is rejected โ€” you don't get a partial result.

So a perfectly correct owner-scoped rule makes getDocs(collection(db, "projects")) fail outright. The right fix is to constrain the query to match the rule:

query(collection(db, "projects"), where("ownerId", "==", user.uid))

The wrong fix โ€” and the tempting one, because it makes the error go away instantly โ€” is loosening the rule until the unconstrained query works. That's how a well-intentioned rule becomes an open collection.

7. Then the things that aren't rules

Prompt to paste into your AI tool:

Review my Firestore and Cloud Storage security rules. For each match block, tell me who can read and who can write, in plain English. Flag anything that allows access to any signed-in user rather than the owner of the record, anything with a date comparison or an unconditional allow, and any collection or subcollection with no matching rule. Then show me corrected rules.

Before you ship

Check the rest of the app

vibecheck scans your project or your live URL for exposed keys and the other mistakes AI-built apps ship with, in plain English โ€” with a fix prompt you paste straight back into your AI tool.

Scan my app โ€” free