How to Write Firebase Security Rules
Protect your Firestore and Realtime Database
TL;DR
Use request.auth.uid to verify the user owns the data. Don't use allow read, write: if true in production. It gives every internet user full access. Test rules in the Firebase console Rules Playground before deploying. One gotcha: Firestore rules and Realtime Database rules use completely different syntax.
Default Rules Are Dangerous
Firebase test-mode rules let anyone read and write everything. They expire after 30 days, but plenty of apps ship with custom rules that replicate the same open access. Check your rules before you launch, and test them by trying to read data that should be protected.
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 has its own JSON-based syntax. It's not interchangeable with Firestore rules:
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?
Four common causes: rules haven't been deployed yet; you're using the Admin SDK, which bypasses rules entirely; the path pattern isn't matching what you expect; or the auth context isn't being passed in the request.
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