How to Test Firebase Security Rules with the Emulator (2026)
The only reliable way to know your Firebase Security Rules work is to try to break them, automatically, every time they change. Reading rules and clicking around the app misses the cases that matter: another user's data, another tenant's data, and a user giving themselves a higher role. The Firebase Emulator Suite and the @firebase/rules-unit-testing library let you write those attempts as ordinary unit tests that run in seconds and never touch production.
This is the technique behind App369's fixed-price Firebase Security Rules Audit, and the kind of test Simon relies on for the rules in Workspace369, his production Flutter and Firebase SaaS. If you want the rules themselves explained first, start with the Firebase Security Rules guide.
Step 1: Install and start the emulators
You need Node.js, a recent Firebase CLI and a Java runtime for the Firestore emulator. The install and configure docs list the exact minimum versions. Then, in your project:
firebase init emulators
firebase emulators:start --only firestore
firebase init emulators lets you pick emulators and downloads their binaries. By default the Firestore emulator listens on port 8080 and the Emulator UI on port 4000. Make sure firebase.json points at your rules file, for example "firestore": { "rules": "firestore.rules" }, plus an emulators block if you want non-default ports. The Firestore emulator docs also recommend demo project IDs (prefixed demo-): code can't accidentally reach production resources, and no real Firebase project is needed.
Step 2: Set up the test library
npm install --save-dev @firebase/rules-unit-testing firebase
The current library (v3) pairs with the modular Firebase JS SDK (v9 and later). You test through RulesTestEnvironment, created with initializeTestEnvironment. The official unit-test guide documents these pieces:
initializeTestEnvironment({ projectId, firestore: { rules } })creates the environment from your rules text.testEnv.authenticatedContext(uid, tokenOptions)acts as a signed-in user;testEnv.unauthenticatedContext()acts as a signed-out client.testEnv.withSecurityRulesDisabled(callback)runs setup code that bypasses rules, so you can seed data.assertSucceeds(promise)andassertFails(promise)check whether rules allowed or denied an operation.testEnv.clearFirestore()clears data between tests, andtestEnv.cleanup()releases resources when you finish.
Step 3: Write a first test file
Say your rules let a user read their own profile, create it only as a plain member, and never change their own role:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow create: if request.auth != null && request.auth.uid == uid
&& request.resource.data.role == 'member';
allow update: if request.auth != null && request.auth.uid == uid
&& request.resource.data.role == resource.data.role;
}
}
}
A Jest-style test file in TypeScript:
import { readFileSync } from 'fs';
import {
initializeTestEnvironment,
assertFails,
assertSucceeds,
RulesTestEnvironment,
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore';
let testEnv: RulesTestEnvironment;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: 'demo-rules-test',
firestore: {
rules: readFileSync('firestore.rules', 'utf8'),
host: '127.0.0.1',
port: 8080,
},
});
});
afterAll(async () => {
await testEnv.cleanup();
});
beforeEach(async () => {
await testEnv.clearFirestore();
// Seed data with rules switched off
await testEnv.withSecurityRulesDisabled(async (ctx) => {
await setDoc(doc(ctx.firestore(), 'users/alice'), { role: 'member' });
});
});
test('a user can read their own profile', async () => {
const alice = testEnv.authenticatedContext('alice');
await assertSucceeds(getDoc(doc(alice.firestore(), 'users/alice')));
});
test('a user cannot read someone else\'s profile', async () => {
const bob = testEnv.authenticatedContext('bob');
await assertFails(getDoc(doc(bob.firestore(), 'users/alice')));
});
test('a signed-out client cannot read anything', async () => {
const anon = testEnv.unauthenticatedContext();
await assertFails(getDoc(doc(anon.firestore(), 'users/alice')));
});
test('a user cannot promote themselves to admin', async () => {
const alice = testEnv.authenticatedContext('alice');
await assertFails(updateDoc(doc(alice.firestore(), 'users/alice'), { role: 'admin' }));
});
Run it while the emulator is up, with your usual test runner. The seed step matters: tests that depend on existing documents must create them with withSecurityRulesDisabled, otherwise your own rules block the setup.
Step 4: Decide what to test
Passing tests for the happy path prove little. Write the attacks:
- Cross-user reads and writes. User B reading, updating and deleting user A's documents, in every collection that holds user data.
- Cross-tenant access. If you have workspaces, teams or organizations, a member of tenant 1 must fail to read or write tenant 2's documents, including sub-collections. Seed two tenants and test both directions.
- Role escalation. A user writing their own
role,isAdminorplanfield on create and on update. Also test adding themselves to another tenant's member list. - Field-level validation. Creates with missing or extra fields, wrong types, oversized strings, and fields the client should never set, such as server-owned timestamps or counters.
- Signed-out access. Every path with
unauthenticatedContext(). - Storage paths. The same user and tenant checks against Storage rules. Pass
storage: { rules }toinitializeTestEnvironmentand usectx.storage()in the tests. - Queries, not just single reads. A rule that allows reading one document can still reject a list query that isn't constrained to what the rule permits. Test the exact query your app runs.
For each tenant-isolation rule, write one test that should succeed and at least one that should fail. If you can't make a failing test fail, you have found a hole.
Step 5: Run it in CI
firebase emulators:exec starts the emulators, runs your command, and shuts them down when the command ends. The emulator docs describe it as the way to run scripted tests, and it suits continuous integration:
firebase emulators:exec --only firestore "npm test"
The Firestore emulator clears its data when it shuts down, so every CI run starts clean. Cache the emulator download between CI runs if your provider supports it, and add the Java runtime to the CI image. Make the rules test a required check for any pull request that touches firestore.rules, storage.rules or the tests.
Step 6: Debug with the request monitor
When a test fails and you don't know which rule caused it, run firebase emulators:start, point a manual client at it, and open the Emulator UI at http://localhost:4000. In the Firestore section, the Requests tab shows each request and traces how your rules were evaluated, which line allowed or denied it. The Firestore emulator docs describe this request monitoring. It is the fastest way to find out that a rule you thought applied never matched.
When to get help
Before you write tests, a quick first pass helps: paste your rules into the free Firebase Security Rules checker to spot public paths, test-mode rules and sign-in-only checks. It runs in your browser.
If you don't have the time to build the model of who should access what, or you have already found a hole and want the fixes reviewed, App369's fixed-price Firebase Security Rules Audit + Fix PR is $950 and delivered in 5 business days. It covers Firestore and Storage rules plus up to 15 Cloud Functions in one Firebase project, and you get a written role and tenant access model, an emulator test suite for cross-user, cross-tenant and role-escalation attempts, a severity-ranked report and a pull request with fixes that passes the suite. It runs on the emulator or test data only, and it is a technical review, not a compliance certificate.
Frequently Asked Questions
How do I test Firestore security rules locally?
Run the Firestore emulator with firebase emulators:start --only firestore, then write tests with @firebase/rules-unit-testing. Create a test environment with initializeTestEnvironment, act as users with authenticatedContext, and check results with assertSucceeds and assertFails.
Can I test Firebase Storage rules the same way?
Yes. Pass a storage: { rules } option to initializeTestEnvironment, start the Storage emulator, and perform uploads and downloads through the context's storage() client. The same assertions apply.
How do I run Firebase rules tests in CI?
Use firebase emulators:exec to start the emulators, run your test command and stop them. For example, firebase emulators:exec --only firestore "npm test". The CI machine needs Node.js, the Firebase CLI and a Java runtime.
How do I add test data when my rules would block it?
Call testEnv.withSecurityRulesDisabled(async (ctx) => { ... }) and write documents through ctx.firestore(). Rules are skipped inside that callback only.
Does passing rules tests mean my app is secure?
No. Tests show your rules behave as you described them, so they are only as good as the cases you wrote. They don't cover Cloud Functions logic, leaked credentials or anything outside Firebase.
Want this done at a fixed price?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
Related Resources
Related Articles
Business Application Modernization for Small Businesses (2026)
Business application modernization for small and mid-size companies: how to tell an app needs it, the options from rehosting to a full rebuild, and how to modernize without stopping the business.
Read more →Apps Built with Flutter: Real Examples by Industry (2026)
Real apps built with Flutter in 2026, grouped by industry: Google Pay, BMW, Toyota, Nubank, Headspace, eBay, MGM Resorts and more, each linked to its case study on flutter.dev.
Read more →