AI & Technology

Firebase Security Rules: The Complete Guide for App Developers (2026)

Simon Dziak
Simon Dziak
Owner & Head Developer
March 7, 2026

Firebase Security Rules are the primary access control mechanism between your users and your data. According to AuditYourApp's Firebase security analysis, roughly 30% of Firebase applications in production have overly permissive rules on at least one collection — meaning nearly one in three apps allows unauthorized reads, writes, or both. Firebase does not enforce any access restrictions by default beyond the initial test/production mode selection, so every permission must be explicitly defined by the developer.

This guide covers the specific rule patterns, validation techniques, and deployment workflows that App369 uses to secure Firebase backends across production applications.

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 provide fine-grained authorization that cannot be bypassed from client code. Unlike client-side validation, which can be disabled by modifying the app, Security Rules execute on Firebase's servers and cannot be circumvented.

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 locked mode. Test mode grants public read and write access to the entire database for 30 days. According to VibeAppScanner's Firebase safety analysis, deploying with test mode rules is the single most common Firebase security failure in production.

Test mode rules look like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      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 locked mode, where all 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.

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 Firestore security documentation, rules evaluate top-down and stop at the first match, so specificity matters.

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, 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 noted in Nadun Kulatunge's advanced Firebase rules guide on Medium, 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']));
}

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 existencehasAll() 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 a rules simulator in the Firebase Console and a local emulator suite for testing rules before deployment. According to Firebase's testing documentation, deploying untested rules is the second most common cause of Firebase security incidents after test mode.

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

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:

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
  // ...
});

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) => {
  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');
    }
  }

  // Process the submission...
});

Cloud Functions complement Security Rules — they do not replace them. Rules should still enforce basic authentication and authorization checks. Functions handle the logic that rules cannot express.

How App369 Secures Firebase Applications

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

  • Locked-mode 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.

FAQ

What happens if I deploy Firebase with test mode rules?

Any user on the internet can read, modify, and delete every document in your database. Test mode grants unrestricted public access using a time-based condition — before the expiry date, anyone with your project ID (which is public) has full access. According to AuditYourApp, roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.

Can Firebase Security Rules replace server-side validation?

Partially. Security Rules can enforce authentication, field-level authorization, type checking, string length constraints, and field immutability. They cannot perform cross-document queries, call external APIs, enforce complex business logic, or implement rate limiting. Use Cloud Functions for operations that exceed what rules can express, and use rules as the first layer of defense for every request.

How do I implement admin-only access in Firebase?

Use Firebase Authentication custom claims. Set { admin: true } on a user's account via the Admin SDK in a Cloud Function, then reference request.auth.token.admin == true in your Security Rules. Do not use a Firestore field like users/{userId}.isAdmin for authorization — clients can modify their own documents if write rules are misconfigured. Custom claims are set server-side and cannot be modified by clients.

How often should I update Firebase Security Rules?

Review and update rules whenever you add a new collection, modify a data schema, or change role definitions. Include rules files in version control and require pull request reviews for all changes. Run the Firebase Emulator test suite in CI before every deployment. At minimum, audit your rules quarterly against the current Firebase Security Rules documentation to ensure they follow updated best practices.

Tags
#firebase security rules #firestore security #firebase authentication #cloud functions #role-based access control #firebase guide
Share:

Related Resources

Related Articles