Skip to content
SealMyApp

Firebase security rules for vibe-coded apps: lock down Firestore and Storage

Your Firebase API key is public by design; Security Rules protect your data. Copy-paste Firestore and Storage rules for apps built with AI tools.

Updated 2026-10-16

Your Firebase web API key is meant to be public; what protects your data is Firebase Security Rules. If your Firestore or Storage rules contain allow read, write: if true, plain allow read, write;, or only check that a user is signed in, anyone can read (and often overwrite) your data — replace them with rules that check who owns each document.

AI builders are good at getting Firebase working and bad at locking it down afterwards. A common pattern: the app shows a "permission denied" error, the AI loosens the rules until the error goes away, and the open rules ship to production. This guide gives you rules to paste in, explains each line, and shows how to test them.

First: your API key isn't the problem

Firebase's own documentation says API keys for Firebase services "only identify your Firebase project and app" and "do not need to be treated as secrets". Data in Firestore, Realtime Database and Storage is protected by Security Rules, and abuse of backend APIs is limited by App Check — not by hiding the key.

One exception: Firebase's docs say not to put a Gemini Developer API key in your app's code. If your app calls Gemini directly from the browser, move that call to a Cloud Function.

Spot the dangerous rules

Open the Firebase console → Firestore Database → Rules (and Storage → Rules). Firebase's insecure rules guide lists these patterns:

You seeWhat it means
allow read, write: if true;Anyone on the internet can read and overwrite everything
allow read, write; (Storage, no condition)Same, for your files
allow read, write: if request.auth != null;Anyone who signs up can read and change everyone's data
allow read, write: if request.time < timestamp.date(…)"Test mode" — open to everyone until the date, then locked to everyone (and your app breaks)

The third one catches people out. Signing up is free and automatic, so "any signed-in user" is barely more private than "anyone".

Copy-paste Firestore rules

Adjust collection and field names to match your app. These assume a users/{uid} profile collection, posts with an authorId field, private orders written only by your server, and a contactMessages form.

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

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

    // Profiles: each user reads and edits only their own,
    // and can never give themselves a role, plan or credits.
    match /users/{userId} {
      allow read: if isUser(userId);
      allow create: if isUser(userId)
        && !request.resource.data.keys().hasAny(['role', 'plan', 'credits']);
      allow update: if isUser(userId)
        && !request.resource.data.diff(resource.data).affectedKeys()
              .hasAny(['role', 'plan', 'credits']);
      allow delete: if false;

      // Orders: owner can read; only your server (Admin SDK) writes.
      match /orders/{orderId} {
        allow read: if isUser(userId);
        allow write: if false;
      }
    }

    // Posts: anyone reads published posts; authors manage their own.
    match /posts/{postId} {
      allow read: if resource.data.published == true
                  || isUser(resource.data.authorId);
      allow create: if isUser(request.resource.data.authorId);
      allow update: if isUser(resource.data.authorId)
                    && request.resource.data.authorId == resource.data.authorId;
      allow delete: if isUser(resource.data.authorId);
    }

    // Contact form: anyone can submit; nobody can read back via the client.
    match /contactMessages/{id} {
      allow create: if request.resource.data.keys()
                         .hasOnly(['name', 'email', 'message', 'createdAt'])
                    && request.resource.data.message is string
                    && request.resource.data.message.size() < 5000;
      allow read, update, delete: if false;
    }

    // Everything not matched above is denied by default.
  }
}

Treat these rules as a starting point, not a drop-in: run them through the Rules Playground or the emulator against your own data model before you publish them.

What the important lines do:

  • isUser(userId) compares the signed-in user's ID with the document's path or owner field. This is the pattern Firebase recommends for content owners.
  • affectedKeys().hasAny([...]) stops a user editing fields that control access or money. Without it, a user can open the console in their browser and set role: "admin" on their own profile.
  • request.resource.data.authorId == resource.data.authorId on update stops someone reassigning a post to another user.
  • allow write: if false on orders means only your server (Admin SDK or Cloud Functions, which bypass rules) can write them. Anything that involves payment status should work this way.
  • Anything not matched is denied. You don't need a catch-all deny rule; just don't add a catch-all allow.

Copy-paste Storage rules

Store each user's files under users/{uid}/…:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // Private files: only the owner can read or write.
    match /users/{userId}/{fileName} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow create, update: if request.auth != null
                            && request.auth.uid == userId
                            && request.resource.size < 5 * 1024 * 1024
                            && request.resource.contentType.matches('image/.*');
      allow delete: if request.auth != null && request.auth.uid == userId;
    }

    // Public avatars: anyone can view, only the owner can upload.
    match /avatars/{userId}/{fileName} {
      allow read;
      allow create, update: if request.auth != null
                            && request.auth.uid == userId
                            && request.resource.size < 2 * 1024 * 1024
                            && request.resource.contentType.matches('image/.*');
    }
  }
}

The size and content-type checks stop someone using your bucket as free hosting for large or non-image files. Delete is separated out because a delete request has no incoming file to check.

"Rules are not filters" — why your app broke

After tightening rules, a screen may show "Missing or insufficient permissions". Usually it's because the code asks for the whole collection:

// Fails once rules are tight: asks for every post, including others' drafts
getDocs(collection(db, 'posts'));

// Works: the query only asks for what the rules allow
getDocs(query(collection(db, 'posts'), where('published', '==', true)));

Firestore rejects a query if it could return a document the user isn't allowed to read. The fix is to change the query, not to loosen the rule. Tell your AI builder exactly that, or it will reach for if true again.

Deploy and test

From the console: paste the rules into the editor and click Publish. Before publishing, use Rules Playground to simulate a read as an unauthenticated user and as two different signed-in users.

From the CLI (if your project has a firebase.json):

firebase deploy --only firestore:rules
firebase deploy --only storage

For anything with payments or personal data, run the rules against the Firebase Emulator Suite with a few unit tests: "user A cannot read user B's profile", "anonymous cannot list contactMessages", "user cannot set their own role".

Then turn on App Check. App Check makes Firestore, Storage, Realtime Database and callable Functions accept only requests from your genuine app — once you switch enforcement on. It's not a substitute for rules (Firebase says it doesn't eliminate all abuse), but it raises the bar for scripts.

Prompt for your AI builder

Rewrite firestore.rules and storage.rules so that: users can only read and
write their own documents (match request.auth.uid against the document path or
owner field); users cannot change role, plan, credits or ownership fields;
payment and order documents are written only by server code; public data is
readable only where a "published" flag is true; Storage uploads are limited to
the owner's folder with a size and content-type check. Never use "if true" or
"request.auth != null" on its own for private data. If a query fails with a
permissions error, fix the query rather than loosening the rules.

FAQ

Is my Firebase API key safe in my frontend code?

Yes, according to Firebase's own docs — it identifies your project, it doesn't grant access. Restrict it in the Google Cloud console to the APIs you use, and keep your Security Rules tight.

Is request.auth != null secure enough?

Only for data every signed-up user should see. For personal data, check request.auth.uid against the owner.

My test mode rules expired and my app stopped working. What now?

Don't extend the date. Replace them with owner-based rules like the ones above, then fix any queries that ask for more than the user can see.

Do Cloud Functions need rules?

The Admin SDK bypasses Security Rules, so your function code must do its own checks — verify the caller's ID token and what they're allowed to do.

What about Realtime Database?

Same principle, different syntax. Rules cascade downwards, so a broad .read: true near the top can't be undone further down. See Firebase's insecure rules guide.

Check it for free

SealMyApp's free scan tests whether a stranger can read your Firestore or Realtime Database or list your Storage files — read-only, after you've proved you own the app, and without keeping any data it sees. Run a free scan.

Sealed — a short weekly security note for vibe coders

One common security mistake in AI-built apps, how to fix it in your builder, and one useful tool. No spam; unsubscribe any time.