Firebase "security rules are defined as public": how to fix it

By the RowLocked team · Updated

Firebase sends a few versions of the same warning: *"Your security rules are defined as public, so anyone can steal, modify, or delete data in your database"*, or *"Client access to your Cloud Firestore database expiring in X days"*. Both mean the same thing: your database currently lets anyone on the internet read and write it, and you need to replace those rules with real ones.

This is common in apps generated by AI tools and quick prototypes, because Firestore's test mode is the easiest way to get started. This guide shows what the default rules do, why the Firebase config in your frontend is not the problem, and how to write rules that keep your app working.

What the warning means

When you create a Firestore database in test mode, Firebase gives you rules like these:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2026, 11, 19);
    }
  }
}

match /{document=**} matches every document in the database. Until the date passes, every read and write is allowed, from anyone, without signing in. After the date, every client request is denied and your app stops working. That is why Firebase emails you: either you are wide open now, or your app is about to break.

The Realtime Database equivalent is even more direct:

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

"But my Firebase API key is secret, right?"

No, and that is fine. The firebaseConfig object in your web app, including apiKey, projectId and databaseURL, is designed to be in client code. It identifies your project; it does not grant access by itself. Firebase's documentation states that API keys for Firebase services are not used to control access to backend resources; Security Rules (and App Check) do that.

The consequence: anyone can copy your config from your site's JavaScript and use the Firebase SDK, or plain REST calls, against your database. With public rules, they can read every document, overwrite data, or delete collections. Hiding the config does not help. Writing rules does.

You can see what an anonymous visitor can read on a Realtime Database by opening this in a browser:

https://YOUR-PROJECT-default-rtdb.firebaseio.com/.json?shallow=true

If it returns data instead of Permission denied, your database is publicly readable.

Step 1: List what each collection should allow

Write one line per collection before touching rules, for example:

  • users/{uid}: a signed-in user can read and write only their own document.
  • posts/{postId}: anyone can read; only the author can create, update, or delete.
  • orders/{orderId}: the buyer can read their own orders; nobody writes from the client (a Cloud Function creates them).
  • admin/...: no client access at all.

Step 2: Replace test mode with real Firestore rules

Here is a starting point that covers those four cases. Paste it into Firestore, then the Rules tab in the Firebase console, or into firestore.rules if you deploy with the CLI.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function signedIn() { return request.auth != null; }
    function isOwner(uid) { return signedIn() && request.auth.uid == uid; }

    // Each user's own profile document
    match /users/{uid} {
      allow read: if isOwner(uid);
      allow create: if isOwner(uid)
        && request.resource.data.keys().hasOnly(['displayName', 'photoURL', 'createdAt']);
      allow update: if isOwner(uid)
        && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['displayName', 'photoURL']);
      allow delete: if false;
    }

    // Public posts, written only by their author
    match /posts/{postId} {
      allow read: if true;
      allow create: if signedIn() && request.resource.data.authorId == request.auth.uid;
      allow update: if signedIn() && resource.data.authorId == request.auth.uid
        && request.resource.data.authorId == resource.data.authorId;
      allow delete: if signedIn() && resource.data.authorId == request.auth.uid;
    }

    // Orders: readable by the buyer, written only by server code
    match /orders/{orderId} {
      allow read: if signedIn() && resource.data.userId == request.auth.uid;
      allow write: if false;
    }

    // Everything else is denied by default (no catch-all match).
  }
}

Key points:

  • There is no `match /{document=}` catch-all.** Firestore denies anything not explicitly allowed, which is what you want.
  • resource.data is the document as it exists now; request.resource.data is what the client is trying to write. Use the first to check ownership and the second to validate the write.
  • The hasOnly checks stop a user from writing fields like role: "admin" or credits: 999999 into their own profile. Firebase calls out this kind of field-level validation in its guidance on insecure rules.
  • Update and delete are separate rules because on a delete request.resource is null, so delete only checks the existing author. The update rule also stops the author from reassigning the post to someone else.
  • Server code using the Firebase Admin SDK bypasses rules, so writes from Cloud Functions keep working with allow write: if false.

Be careful with allow read, write: if request.auth != null; on everything. It is better than public, but if anyone can sign up for your app (including with anonymous auth), it means any stranger with an account can read and edit all data.

Step 3: Realtime Database rules

For Realtime Database, the per-user pattern looks like this:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid === $uid",
        ".write": "auth != null && auth.uid === $uid"
      }
    },
    "publicPosts": {
      ".read": true,
      "$postId": {
        ".write": "auth != null && (!data.exists() || data.child('authorId').val() === auth.uid)",
        ".validate": "newData.child('authorId').val() === auth.uid"
      }
    }
  }
}

Realtime Database rules cascade: a .read: true at a parent path grants read on everything below it, and a child rule cannot take it away. So keep broad grants at the lowest level possible, and never put .read: true at the root.

Step 4: Cloud Storage rules

Firebase Storage has its own rules, often also left open. Keep user uploads under their UID and limit size and type:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{uid}/{fileName} {
      allow read: if request.auth != null && request.auth.uid == uid;
      allow write: if request.auth != null && request.auth.uid == uid
        && request.resource.size < 10 * 1024 * 1024
        && request.resource.contentType.matches('image/.*|application/pdf');
    }
  }
}

Step 5: Test before you deploy

  • Use the Rules Playground in the Firebase console (Rules tab) to simulate reads and writes as an unauthenticated user and as a specific user ID.
  • For anything non-trivial, use the Firebase Local Emulator Suite and write unit tests for your rules so future edits do not reopen access.
  • After deploying, use your app normally as two different test users and confirm each sees only their own data.
  • Re-check from the outside: open the Realtime Database .json URL above and confirm it says permission denied, or run RowLocked's free scan, which checks for public Firebase reads when it finds a Firebase config in your site.

Consider App Check too

Firebase App Check helps make sure requests come from your real app rather than scripts, which reduces abuse like scraping and spam. It is a useful extra layer, but it is not a replacement for rules: rules decide what each user may do, App Check makes it harder for non-app clients to call at all.

A prompt for your AI tool

Replace the Firestore test-mode rules with production rules. Remove any match /{document=**} rule. For each collection this app uses, write rules so users can only read and write their own documents (match on request.auth.uid), validate written fields with hasOnly so users cannot set roles or credits, allow public read only where the data is meant to be public, and deny client writes to anything created by Cloud Functions. Do the same for Storage rules. Explain each rule in plain English.

Frequently asked questions

Can I just extend the test mode date?

You can, and it stops the emails for a while, but your database stays fully public until then. Only do that for a throwaway prototype with no real data.

Do I need to hide my Firebase apiKey?

No. It is meant to be public. Restrict it in the Google Cloud console to the APIs and websites you use if you like, but your data is protected by Security Rules.

Why does my app show "Missing or insufficient permissions" after I changed the rules?

Your rules now deny a request the app makes. Check which collection and operation fail in the browser console, then add a rule that allows exactly that for the right user, rather than reopening everything.

Do rules filter query results automatically?

No. Firestore rules are not filters: a query must be constrained so that every document it could return is allowed, for example where('authorId', '==', uid). Otherwise the whole query is rejected.

Related guides

All guides

Check my app now