How to Write Firebase Security Rules
Protect your Firestore and Realtime Database
TL;DR
TL;DR: Use request.auth.uid to verify the user owns the data. Never use allow read, write: if true in production. Test rules in the Firebase console before deploying. Firestore rules are different from Realtime Database rules.
Default Rules Are Dangerous
Firebase projects often start with "test mode" rules that allow anyone to read and write all data. These rules expire after 30 days, but many developers forget to update them. Always check your rules before launching.
Firestore Security Rules
Basic Structure
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Rules go here
}
}
Pattern 1: User-Owned Documents
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only access their own documents
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
// Todos owned by users
match /todos/{todoId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
}
}
Pattern 2: Public Read, Authenticated Write
match /posts/{postId} {
// Anyone can read published posts
allow read: if resource.data.status == 'published';
// Only authenticated users can create posts
allow create: if request.auth != null
&& request.resource.data.authorId == request.auth.uid;
// Only the author can update/delete
allow update, delete: if request.auth != null
&& resource.data.authorId == request.auth.uid;
}
Pattern 3: Data Validation
match /products/{productId} {
allow create: if request.auth != null
// Required fields
&& request.resource.data.keys().hasAll(['name', 'price', 'createdAt'])
// Type validation
&& request.resource.data.name is string
&& request.resource.data.price is number
&& request.resource.data.price > 0
// Timestamp validation
&& request.resource.data.createdAt == request.time;
}
Pattern 4: Role-Based Access
// Assumes users have a 'role' field in their profile
match /admin/{document=**} {
allow read, write: if request.auth != null
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
// Or use custom claims (set via Admin SDK)
match /admin/{document=**} {
allow read, write: if request.auth != null
&& request.auth.token.admin == true;
}
Realtime Database Rules
Realtime Database uses a different JSON-based syntax:
Basic Structure
{
"rules": {
".read": false,
".write": false,
// Specific rules override these defaults
}
}
User-Owned Data
{
"rules": {
"users": {
"$userId": {
".read": "$userId === auth.uid",
".write": "$userId === auth.uid"
}
},
"todos": {
"$todoId": {
".read": "data.child('userId').val() === auth.uid",
".write": "(!data.exists() && newData.child('userId').val() === auth.uid) ||
(data.child('userId').val() === auth.uid)"
}
}
}
}
Testing Your Rules
Firebase Console
- Go to Firebase Console → Firestore → Rules
- Click "Rules Playground"
- Select operation type (get, list, create, update, delete)
- Enter document path and optional auth context
- Run the simulation
Emulator Testing
// Install Firebase emulators
firebase init emulators
// Start emulators
firebase emulators:start
// In your test file
import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing';
describe('Firestore rules', () => {
it('allows users to read their own data', async () => {
const db = getTestFirestore({ uid: 'user123' });
await assertSucceeds(db.collection('users').doc('user123').get());
});
it('denies users from reading others data', async () => {
const db = getTestFirestore({ uid: 'user123' });
await assertFails(db.collection('users').doc('other-user').get());
});
});
Common Mistakes
Never Do This in Production
// DANGEROUS: Anyone can read/write everything
match /{document=**} {
allow read, write: if true;
}
// DANGEROUS: Allows all authenticated users full access
match /{document=**} {
allow read, write: if request.auth != null;
}
| Mistake | Problem | Fix |
|---|---|---|
| if true | Anyone can access | Always check auth and ownership |
| if request.auth != null | Any logged-in user can access | Verify user owns the data |
| No validation on create | Users can set any userId | Validate request.resource.data.userId |
| Recursive wildcards | {document=**} is too broad | Be specific about paths |
What's the difference between resource and request.resource?
resource.data contains the existing document data. request.resource.data contains the data being written. Use resource for read/delete operations and request.resource for create/update validation.
Why are my rules not working?
Common issues: 1) Rules haven't been deployed yet, 2) You're using the Admin SDK which bypasses rules, 3) Path matching is incorrect, 4) Auth context isn't being passed correctly.
How do I debug rules?
Use the Rules Playground in Firebase Console, check the Firebase logs in Google Cloud Console, or use the Firebase emulator with debug logging enabled.
Related guides:Firebase Security Guide · Firebase Auth Rules · Supabase vs Firebase Security