Launch and other stacks
Firebase security rules basics: lock down Firestore and Storage
Firebase security rules are the only thing deciding who can read and write your Firestore, Realtime Database and Cloud Storage data, because your app’s Firebase config is public by design. Replace any if true or test-mode request.time < … rule with rules that compare request.auth.uid with the document’s owner, and test them in the emulator as a visitor, a stranger and the owner before deploying.
TL;DR
- Your
apiKeyandprojectIdare public. Rules are your security. - Test mode opens everything for 30 days.
if request.auth != nullopens everything to anyone who signs up. - Match on ownership:
request.auth.uid == userId, orresource.data.ownerId == request.auth.uid. - Test with
@firebase/rules-unit-testingand the emulator; keep the rules in your repo and deploy from there.
Why rules are your whole security model
A Firebase web app talks to Firestore, Realtime Database and Cloud Storage directly from the browser using a config object with an apiKey. That key identifies your project; it doesn’t grant access, and it isn’t secret. Anyone can copy it from your site and use the Firebase SDK against your project. What they can do is decided entirely by your security rules, evaluated by Google’s servers on every request.
Firebase Admin SDKs on servers bypass rules, the same way Supabase’s service role key bypasses RLS, so admin credentials must never reach the browser either.
The rules that leak
// 1. Open to everyone, forever
allow read, write: if true;
// 2. "Test mode": open to everyone until the date
allow read, write: if request.time < timestamp.date(2026, 12, 31);
// 3. Open to anyone who signs up
allow read, write: if request.auth != null;Test mode is what the Firebase console offers when you create a database, so you can build without rules for 30 days. Apps ship with it, or the date gets pushed forward when it expires and the app stops working. The third form is Firebase’s version of PL-002: if anyone can create an account, “signed in” means anyone.
Realtime Database uses JSON rules; the equivalent of rule 1 is {"rules": {".read": true, ".write": true}}.
How rules evaluate
matchblocks select document paths.{userId}captures a path segment;{document=**}matches everything below.allowgrants an operation if its condition is true:read(orgetandlist) andwrite(orcreate,update,delete).- If any matching
allowis true, the request is allowed. A broad{document=**}rule overrides careful rules elsewhere. request.authis the signed-in user (or null).resource.datais the document as stored;request.resource.datais the document as it would be after a write.- Rules are not filters: a query must ask only for documents the rules allow, or the whole query is refused. Query with
where('ownerId', '==', uid)to match an ownership rule.
Owner-only rules to start from
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; }
// A user's own profile, keyed by their uid
match /users/{userId} {
allow read, update, delete: if isOwner(userId);
allow create: if isOwner(userId);
}
// Documents with an ownerId field
match /notes/{noteId} {
allow read, delete: if isOwner(resource.data.ownerId);
allow create: if isOwner(request.resource.data.ownerId);
// Owners can edit, but can't hand the note to someone else
allow update: if isOwner(resource.data.ownerId)
&& request.resource.data.ownerId == resource.data.ownerId;
}
// Public to read, written only by the author
match /posts/{postId} {
allow read: if true;
allow create: if isOwner(request.resource.data.authorId);
allow update, delete: if isOwner(resource.data.authorId);
}
}
}Anything not matched is denied, so there is no need for a catch-all. Never store a role on a document the user can update; for admins, use custom claims set from a server with the Admin SDK and check request.auth.token.admin == true.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Each user reads and writes only under users/<their uid>/
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}Test the rules before you deploy
The emulator runs your rules locally, and @firebase/rules-unit-testing lets you act as any user. This test seeds one private document, then checks a visitor and a stranger are refused while the owner can still read:
// npm i -D firebase @firebase/rules-unit-testing
// run: firebase emulators:exec --only firestore "node rules.test.mjs"
import { readFileSync } from 'node:fs';
import { initializeTestEnvironment, assertFails, assertSucceeds } from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc } from 'firebase/firestore';
const env = await initializeTestEnvironment({
projectId: 'demo-rules-check',
firestore: { rules: readFileSync('firestore.rules', 'utf8') },
});
await env.withSecurityRulesDisabled(async (ctx) => {
await setDoc(doc(ctx.firestore(), 'users/owner'), { email: 'owner@example.com' });
});
const visitor = env.unauthenticatedContext().firestore();
const stranger = env.authenticatedContext('stranger').firestore();
const owner = env.authenticatedContext('owner').firestore();
await assertFails(getDoc(doc(visitor, 'users/owner')));
await assertFails(getDoc(doc(stranger, 'users/owner')));
await assertSucceeds(getDoc(doc(owner, 'users/owner')));
await assertFails(setDoc(doc(stranger, 'users/owner'), { email: 'x' }));
console.log('rules pass: visitor and stranger denied, owner reads');
await env.cleanup();A demo- project id keeps the emulator fully offline. Run the test in CI so a later change can’t quietly reopen the database.
Keep rules in the repo, deploy from the repo
Rules edited in the Firebase console and never copied back drift away from what your repo says, and from what anyone reviewing the repo will see. Keep firestore.rules and storage.rules in the repo, referenced from firebase.json, and deploy with:
firebase deploy --only firestore:rules,storageThe console’s Rules Playground is handy for one-off checks, but it doesn’t replace a test that runs on every change.
App Check is not a rule
Firebase App Check makes it harder for scripts outside your app to call your backend. It is a useful extra layer, but a determined attacker can still act through your real app, so it doesn’t make open rules safe. Fix the rules first.
Sources
- Firebase: Security Rules
- Firebase: get started with Cloud Firestore rules
- Firebase: build unit tests for rules
Related findings
The checks behind this guide, each with example code, a self-check and the fix.
Free check
Check your app for this, free.
Paste your public repo. PatchLabs rebuilds your database from its migrations, tests it as a visitor, a stranger and the owner, reads the code for keys and risky patterns, and hands you the fix.
Written by Manraj Singh Dhillon. Last checked 10 Nov 2026. Code samples are synthetic and run against your own project; nothing here names a real app. Spotted a mistake? Tell us.