← All guides

Firebase Security Rules for AI-Built Apps

Updated 2026-10-14

If your app uses Firebase, your users' browsers talk to Firestore and Cloud Storage directly. There's no server in between. The only thing deciding who can read or write what is your Security Rules.

AI coding tools and quick-start tutorials usually set up Firebase in "test mode" to get things working. That's fine for a weekend, and dangerous in production. This guide shows how to check your rules and replace them with ones that actually protect data.

First: the Firebase API key is not the problem

People often panic when they see their Firebase config in frontend code:

const firebaseConfig = {
  apiKey: "AIza...",
  authDomain: "your-app.firebaseapp.com",
  projectId: "your-app",
  // ...
};

This is expected. The web apiKey identifies your project; it doesn't grant access to data. Access is controlled by Security Rules and, optionally, App Check. So "my Firebase key is public" is not a leak. "My rules allow everyone" is.

What is secret: service account JSON files used by the Admin SDK. Those bypass rules entirely and must never be in a client app or a public repo. If one leaked, see how to rotate leaked keys.

Step 1: Find out what your rules say now

Open the Firebase console, go to Firestore Database > Rules and Storage > Rules, or look for firestore.rules and storage.rules in your repo. Watch for these patterns.

Test mode (expires, then locks everything or gets extended forever):

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

Wide open:

allow read, write: if true;

Any logged-in user can do anything:

allow read, write: if request.auth != null;

That last one looks safe but often isn't. If anyone can sign up, then anyone can read every user's documents.

Step 2: Start from deny-by-default

Firestore rules deny everything that isn't explicitly allowed. Build up from a locked file:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Nothing is allowed unless a rule below allows it.
  }
}

Then add one match block per collection.

Step 3: Owner-only documents

The most common pattern is "users can only see and edit their own data". Store the owner's UID on the document, or use it as the document ID.

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;
    }

    // One profile doc per user, at /users/{uid}
    match /users/{uid} {
      allow read: if isOwner(uid);
      allow create: if isOwner(uid);
      allow update: if isOwner(uid);
      allow delete: if false;
    }

    // Notes owned by a user, with an ownerId field
    match /notes/{noteId} {
      allow read, delete: if isOwner(resource.data.ownerId);
      allow create: if isOwner(request.resource.data.ownerId);
      allow update: if isOwner(resource.data.ownerId)
                    && request.resource.data.ownerId == resource.data.ownerId;
    }
  }
}

Key details:

Step 4: Protect privileged fields

If your users document includes a role, plan or credits field, the owner-only rule above lets users edit it themselves. Use diff() to restrict which fields a user may change:

match /users/{uid} {
  allow update: if isOwner(uid)
    && request.resource.data.diff(resource.data).affectedKeys()
         .hasOnly(['displayName', 'photoURL', 'bio']);
}

Set fields like plan only from trusted server code (Cloud Functions or your backend using the Admin SDK), for example after a verified Stripe webhook. If you're using Stripe, see Stripe webhook signature verification in Next.js.

For admin access, prefer custom claims set with the Admin SDK and checked as request.auth.token.admin == true. Users can't set custom claims themselves.

Step 5: Validate data shape

Rules can also reject bad data:

allow create: if isOwner(request.resource.data.ownerId)
  && request.resource.data.keys().hasOnly(['ownerId', 'title', 'body', 'createdAt'])
  && request.resource.data.title is string
  && request.resource.data.title.size() <= 200
  && request.resource.data.createdAt == request.time;

This stops users from stuffing unexpected fields or huge strings into your database.

Step 6: Watch out for subcollections and wildcards

Rules don't cascade. A rule on /users/{uid} does not cover /users/{uid}/private/{doc}. Each subcollection needs its own match, or a recursive wildcard you've thought through:

match /users/{uid}/{document=**} {
  allow read, write: if isOwner(uid);
}

Be careful with {document=**} at the top level. A broad recursive rule like match /{document=**} { allow read: if true; } overrides everything, because if any rule allows a request, it's allowed.

Also remember rules are not filters. A query must only ask for documents the user is allowed to read, or the whole query is rejected. So queries need to include where("ownerId", "==", user.uid) to match an owner-only rule.

Step 7: Lock down Cloud Storage

Storage has its own rules file. Owner-only uploads with size and type limits:

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 < 5 * 1024 * 1024
        && request.resource.contentType.matches('image/.*');
    }
  }
}

Step 8: Test your rules before deploying

Use the Firebase Emulator Suite and the @firebase/rules-unit-testing package to write tests that try to break your rules:

import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing";
import { readFileSync } from "node:fs";
import { doc, getDoc } from "firebase/firestore";

const env = await initializeTestEnvironment({
  projectId: "demo-test",
  firestore: { rules: readFileSync("firestore.rules", "utf8") },
});

const alice = env.authenticatedContext("alice").firestore();
const bob = env.authenticatedContext("bob").firestore();

await assertSucceeds(getDoc(doc(alice, "users/alice")));
await assertFails(getDoc(doc(bob, "users/alice")));

The console's Rules Playground is also handy for quick checks. Test at least: logged out, logged in as the owner, and logged in as a different user.

Step 9: Add App Check

Firebase App Check helps make sure requests come from your real app rather than a script. It isn't a replacement for rules, but it raises the bar against automated abuse. Turn on enforcement once you've confirmed your app sends valid tokens.

Before you launch

For the rest of your launch prep, see the pre-launch security checklist for AI-built apps.

Check the rest of your repo

Rules are one piece. PreflightPR scans repos for leaked secrets, unprotected server routes and payment webhook problems that often sit alongside a Firebase setup. Run a free scan on a public repo to get a launch score and a list of fixes. It's an automated best-effort review, not a substitute for testing your rules.

Check your repo in about a minute

Paste a public GitHub URL and get a free launch score.

Run a free scan

More guides