Skip to content

AI & TechnologyFirebase Security Rules Best Practices: The Complete Guide (2026)

Simon Dziak
Simon Dziak
Founder & developer, App369
LinkedIn
Published March 7, 2026 · Updated October 25, 2026Updated October 25, 2026Facts last verified October 21, 202611 min

Disclosure: App369 publishes this guide and sells a fixed-price Firebase security rules audit.

Key takeaways

  • Firebase says test mode "allows anyone to read and overwrite your data", while production mode denies all reads and writes from mobile and web clients. Start from the deny-all baseline and add access per collection.
  • If several match statements cover a request, access is allowed when any one of them allows it, so a broad rule cannot be narrowed by a more specific one.
  • On updates, check both resource.data (the stored document) and request.resource.data (the incoming one), or a user can hand their document to someone else or add fields you never validated.
  • Rules can read other documents with get() and exists(): 10 calls for a single-document request or query, 20 for batched writes and transactions. Each call is billed as a document read, even when the request is denied.
  • Server client libraries, including the Admin SDK in Cloud Functions, bypass Security Rules, so functions need their own authorization checks.
  • Test rules with the Emulator Suite and @firebase/rules-unit-testing before every deploy, using assertSucceeds and assertFails.

Pricing an app? Try the free app cost calculator or get a range for your project in the instant quote.

Firebase Security Rules are the primary access control mechanism between your users and your data. AuditYourApp says its own scanning data shows roughly 30% of Firebase apps in production have overly permissive rules on at least one collection. When you create a database, Firebase lets you choose production mode, which denies all client access, or test mode, which allows anyone to read and overwrite your data until an expiry date, so every permission must be explicitly defined by the developer. The best practices that follow are summarised in one table first, then explained with Firebase’s own documentation.

This guide covers the specific rule patterns, validation techniques, and deployment workflows that App369 uses to secure Firebase backends across production applications. To check rules you already have, paste them into the free Firebase Security Rules checker, which runs in your browser.

Firebase Security Rules Best Practices at a Glance

Each row is explained, with the official source, further down this guide.

Best practiceWhat goes wrong without itHow to apply it
Start from production mode (deny all)Test mode lets anyone read and overwrite every document until its expiry dateBegin with allow read, write: if false; and open access per collection
Keep each match narrowAccess is allowed when any matching rule allows it, so a broad rule can’t be narrowed laterWrite rules per collection path; avoid a top-level wildcard that allows reads or writes
Check both documents on updateA user can hand a document to someone else or add fields you never validatedCompare resource.data with request.resource.data and restrict changed keys
Use custom claims for rolesAn isAdmin field that clients can write makes anyone an adminSet claims like { admin: true } with the Admin SDK; check request.auth.token.admin
Validate types and sizesClients can store wrong types, huge strings or extra fieldsCheck types, lengths and allowed keys in the rule
Budget get() and exists()Each call is billed as a read, even on denied requests, and is capped at 10 per request (20 for batches)Keep lookups few; move heavy checks to custom claims or server code
Guard Cloud Functions separatelyThe Admin SDK bypasses Security Rules entirelyCheck the caller’s identity and role inside every function
Test before every deployA rules change can lock users out or open data, and you only find out in productionRun the Emulator Suite with @firebase/rules-unit-testing (assertSucceeds, assertFails)

Why Firebase Security Rules Matter

Firebase Security Rules operate as a server-side access control layer that evaluates every read and write request before it reaches your database. Without properly configured rules, any user with your Firebase project’s configuration (which is embedded in every client app and publicly visible) can read, modify, or delete data directly through the Firebase SDK or REST API.

According to Firebase’s official documentation, Security Rules stand between your data and malicious users, and Firebase’s rules overview says clients aren’t responsible for enforcing security. Unlike client-side validation, which can be disabled by modifying the app, Security Rules are enforced on Firebase’s servers.

Rules apply to three Firebase services independently:

  • Cloud Firestore: document-level read/write control with path-based matching
  • Realtime Database: node-level access with cascading permissions
  • Cloud Storage: file-level access based on path, metadata, and authentication state

Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.

The Test Mode Trap: Firebase’s Biggest Vulnerability

Firebase’s setup wizard offers two starting configurations: test mode and production mode. Test mode grants public read and write access to the entire database until an expiry date. VibeAppScanner, which sells a scanner, calls test mode the number one cause of Firebase data breaches.

Test mode rules look like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Test mode: open to everyone until a date set when the database was created
      allow read, write: if request.time < timestamp.date(2026, 4, 7);
    }
  }
}

The timestamp creates an illusion of safety: after the date passes, all access is blocked, breaking the app entirely. Before the date, anyone on the internet can read and overwrite every document. Both outcomes are unacceptable in production.

The correct starting point is production mode, where all client access is denied by default:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.

AI app builders often leave test-mode rules in place instead of a deny-all baseline like this one. If your Lovable, Bolt or Cursor app has open data, sign-in loops or failing deploys, the AI-built app rescue starts with a $450 Launch Check. On Supabase rather than Firebase, the free Supabase October 30 grants checker lists the tables that need explicit grants since that date. Builder-specific checklists: Lovable, Bolt, Replit and Cursor. If Firebase has emailed you that your database isn’t properly secured, the insecure rules alert fix covers what to change.

Writing Firestore Security Rules That Actually Work

Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to Firebase’s rules structure documentation, if several rules match a request, access is allowed when any one of them allows it, so a broad rule can’t be narrowed by a more specific one. Keep each allow condition as tight as it can be.

Here is a production-ready pattern for a user-owned data collection:

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

    // Users can only read and write their own profile
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // Posts are publicly readable, but only the author can write
    match /posts/{postId} {
      allow read: if true;
      allow create: if request.auth != null
                    && request.resource.data.authorId == request.auth.uid;
      allow update: if request.auth != null
                    && resource.data.authorId == request.auth.uid
                    && request.resource.data.authorId == resource.data.authorId;
      allow delete: if request.auth != null
                    && resource.data.authorId == request.auth.uid;
    }

    // Admin-only collection
    match /adminConfig/{docId} {
      allow read, write: if request.auth != null
                         && request.auth.token.admin == true;
    }
  }
}

Key principles for effective rules:

  • Separate read and write permissions. Use allow read and allow write independently. Break write into create, update, and delete when different logic applies.
  • Always check request.auth != null. This confirms the user is authenticated. Without this check, unauthenticated users can access the data.
  • Use resource.data for existing document fields and request.resource.data for incoming data in create/update operations.
  • Avoid wildcards at the root level. The pattern match /{document=**} applies to every document in the database. Use it only with if false as a catch-all deny rule.

Structure your database with security in mind. Organize collections by permission boundary: keep admin-only data in separate collections rather than mixing it with user-readable documents.

Role-Based Access Control with Custom Claims

Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to Firebase’s custom claims documentation, claims are set server-side using the Admin SDK and propagated to the client through the ID token.

Set custom claims from a Cloud Function or admin server:

const admin = require('firebase-admin');

// Assign admin role
await admin.auth().setCustomUserClaims(userId, { admin: true });

// Assign editor role with department
await admin.auth().setCustomUserClaims(userId, {
  role: 'editor',
  department: 'marketing'
});

Reference claims in Security Rules:

match /content/{docId} {
  // Admins have full access
  allow read, write: if request.auth.token.admin == true;

  // Editors can read and update, but not delete
  allow read, update: if request.auth.token.role == 'editor';

  // All authenticated users can read
  allow read: if request.auth != null;
}

As Firebase’s custom claims documentation advises, custom claims should be limited to authorization data only: role identifiers, permission flags, and department codes. Do not store user preferences or profile data in claims, as the token has a size limit and claims are only refreshed when the token is re-issued (typically every hour).

Implementation guidelines:

  • Limit claim size. Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.
  • Force token refresh after claim changes. Call user.getIdToken(true) on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.
  • Use hierarchical roles. Define roles with clear inheritance (viewer < editor < admin) so rules remain readable as the permission model grows.

Data Validation in Security Rules

Security Rules can enforce data structure requirements directly, preventing malformed data from reaching the database regardless of what the client sends. This is the first line of defense against both bugs and deliberate manipulation.

Validation patterns for common requirements:

match /users/{userId} {
  allow create: if request.auth.uid == userId
    // Required fields must exist
    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])
    // Name must be a string between 2 and 100 characters
    && request.resource.data.name is string
    && request.resource.data.name.size() >= 2
    && request.resource.data.name.size() <= 100
    // Email must be a string and match the auth email
    && request.resource.data.email is string
    && request.resource.data.email == request.auth.token.email
    // createdAt must be a timestamp
    && request.resource.data.createdAt is timestamp
    // No unexpected fields allowed
    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);

  allow update: if request.auth.uid == userId
    // Prevent changing the email field
    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']))
    // Keep the same shape and types as on create
    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl'])
    && request.resource.data.name is string
    && request.resource.data.name.size() >= 2
    && request.resource.data.name.size() <= 100;
}

Validation capabilities in Firestore rules include:

  • Type checking: verify that fields are string, int, float, bool, timestamp, list, or map
  • String length: use .size() to enforce minimum and maximum lengths
  • Field existence: hasAll() for required fields, hasOnly() to prevent extra fields
  • Field immutability: use .diff().affectedKeys() to prevent modification of specific fields after creation
  • Numeric ranges: enforce minimum and maximum values for numbers
  • List constraints: check .size() on lists to limit array lengths

Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.

Testing and Deploying Rules Safely

Firebase provides the Rules Playground in the Firebase Console and a local emulator suite for testing rules before deployment. Deploying untested rules is a common way for mistakes to reach production; Firebase’s testing documentation shows how to unit test rules with the emulator. For a step-by-step version, see how to test Firebase Security Rules.

For a quick first pass, paste your rules into App369’s free Firebase Security Rules checker. It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.

Use the Firebase Emulator Suite for local testing:

firebase emulators:start --only firestore

Write unit tests with the @firebase/rules-unit-testing package:

const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase/rules-unit-testing');

const testEnv = await initializeTestEnvironment({
  projectId: 'your-project-id',
  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }
});

// Test: authenticated user can read their own document
const userContext = testEnv.authenticatedContext('user123');
await assertSucceeds(
  userContext.firestore().collection('users').doc('user123').get()
);

// Test: user cannot read another user's document
await assertFails(
  userContext.firestore().collection('users').doc('other-user').get()
);

// Test: unauthenticated user cannot write
const unauthedContext = testEnv.unauthenticatedContext();
await assertFails(
  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })
);

Deployment workflow:

  1. Write rules locally in firestore.rules, database.rules.json, and storage.rules
  2. Test with the emulator: run the full test suite against the local emulator before every deployment
  3. Use firebase deploy --only firestore:rules to deploy rules independently from other Firebase resources
  4. Version control your rules files: commit them to Git and review changes in pull requests
  5. Monitor denied requests in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules. When your own app gets “Missing or insufficient permissions” after a rules change, this fix page lists the causes in the order to check them

Server-Side Validation with Cloud Functions

Security Rules handle authorization and basic data validation, but complex business logic requires server-side enforcement through Cloud Functions. Cloud Functions execute in a trusted environment where the code cannot be inspected or modified by clients.

Use Cloud Functions as a second validation layer for the cases below. The samples use the 1st gen style, functions.https.onCall(async (data, context) => ...). In 2nd gen the handler takes a single request object with request.data and request.auth, so adapt them for new code.

Financial transactions: verify amounts, check account balances, and enforce rate limits server-side:

exports.processPayment = functions.https.onCall(async (data, context) => {
  // Verify authentication
  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');

  // Server-side amount validation
  if (data.amount <= 0 || data.amount > 10000) {
    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');
  }

  // Check user's balance from a trusted source
  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();
  if (userDoc.data().balance < data.amount) {
    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');
  }

  // Process the transaction with admin privileges. Read the balance and write the charge in one Firestore transaction, or two parallel requests can both pass the check above.
  // ...
});

Data aggregation and denormalization: use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:

exports.onPostCreated = functions.firestore
  .document('posts/{postId}')
  .onCreate(async (snap, context) => {
    // Increment the author's post count atomically
    const authorId = snap.data().authorId;
    await admin.firestore().collection('users').doc(authorId).update({
      postCount: admin.firestore.FieldValue.increment(1)
    });
  });

Rate limiting: prevent abuse by tracking request frequency per user:

exports.submitFeedback = functions.https.onCall(async (data, context) => {
  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
  const userId = context.auth.uid;
  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();

  if (lastSubmission.exists) {
    const lastTime = lastSubmission.data().timestamp.toMillis();
    if (Date.now() - lastTime < 60000) { // 1 minute cooldown
      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');
    }
  }

  // Record the time, then process the submission. Use a transaction if two
  // requests could arrive at once.
  await admin.firestore().collection('rateLimits').doc(userId).set({
    timestamp: admin.firestore.Timestamp.now()
  });
  // Process the submission...
});

Cloud Functions complement Security Rules: they do not replace them. Server client libraries, including the Admin SDK that functions use, bypass Security Rules entirely, so a function that writes with admin privileges has to do its own authorization checks. Rules should still enforce basic authentication and authorization checks. Functions handle the logic that rules cannot express.

Firebase App Check adds a separate layer: once enforced, Cloud Firestore, Realtime Database, Cloud Storage and callable Cloud Functions reject requests that lack a valid attestation token from your real app. Firebase describes App Check and Authentication as complementary and says App Check does not stop all abuse, so it does not replace rules.

How App369 Secures Firebase Applications

App369 implements Firebase security as a standard component of every project that uses the Firebase stack:

  • Production-mode (deny-all) defaults on all new projects, with permissions added per-collection based on documented access requirements
  • Custom claims-based RBAC for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions
  • Comprehensive data validation in rules for every writable collection: type checking, field constraints, and immutability guards
  • Automated rules testing in CI/CD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment
  • Server-side business logic via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access
  • Security audits against the OWASP Mobile Top 10 before production release, with specific attention to Firebase configuration

App369 builds Flutter applications with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on Flutter security best practices. If your app is already live, the Firebase Security Rules Audit + Fix PR runs an emulator test suite that tries cross-user and cross-tenant reads and writes, then fixes what it finds in a pull request.

Frequently asked questions

What happens if I deploy Firebase with test mode rules?
Until the expiry date in the rule, anyone on the internet can read, change and delete every document, because your project ID ships inside your app. After the date, every request is denied and the app stops working. Start from production mode, which denies client access, and add permissions per collection.
Can Firebase Security Rules replace server-side validation?
Partially. Rules can enforce authentication, field-level authorization, types, string lengths and immutable fields, and can read a few other documents with get() and exists(). They cannot run queries, call external APIs or rate-limit, so use Cloud Functions for that and keep rules as the first layer for every request.
How do I implement admin-only access in Firebase?
Set a custom claim such as { admin: true } with the Admin SDK from trusted server code, then check request.auth.token.admin == true in your rules. Don't rely on an isAdmin field in a user document that clients may be able to write.
Do Firebase Security Rules apply to the Admin SDK and Cloud Functions?
No. Firebase says the server client libraries bypass all Cloud Firestore Security Rules. A function that writes with admin privileges has to check authorization itself.
How big can Firebase custom claims be?
The claims payload must not exceed 1000 bytes. Keep claims to role identifiers and permission flags, and call getIdToken(true) if the client needs a changed claim before its one-hour ID token refreshes.
Does App Check replace Security Rules?
No. App Check checks that a request comes from your real app, and Authentication identifies the user. Firebase describes them as complementary, and you still need rules to decide who can read or write what.
How often should I update Firebase Security Rules?
Whenever you add a collection, change a data schema or change role definitions. Keep the rules file in version control, review changes like code, and run the emulator tests before every deploy.

Sources

  1. Firebase docs, Get started with Cloud Firestore Security Rules
  2. Firebase docs, Structure Cloud Firestore Security Rules
  3. Firebase docs, Writing conditions for Cloud Firestore Security Rules
  4. Firebase docs, Control access with custom claims
  5. Firebase docs, Firebase App Check
  6. Firebase docs, Build unit tests for Security Rules
  7. Firebase docs, Understand Cloud Firestore billing
Tags
#firebase security rules #firestore security #firebase authentication #cloud functions #role-based access control #firebase guide
Share:

Related Resources

Related guides