Firebase Realtime Database rules use a JSON-based structure with .read, .write, and .validate properties at each node. Secure your database by requiring authentication with auth !== null, restricting writes to document owners with auth.uid === $uid, validating data types and lengths with .validate, and adding .indexOn for query performance. Deploy rules from the Firebase Console or CLI and test them with the Rules Playground.
Securing Firebase Realtime Database with Rules
Firebase Realtime Database security rules control who can read and write data at each path in your JSON tree. Unlike Firestore rules, RTDB rules use a JSON structure with .read, .write, and .validate keys. This tutorial covers the rules syntax, authentication-based access control, data validation, owner-only patterns, role-based access, indexing for queries, and testing rules before deploying to production.
Prerequisites
- A Firebase project with Realtime Database created
- Firebase Authentication enabled with at least one sign-in method
- Access to the Firebase Console
- Firebase CLI installed (optional, for CLI-based deployment)
Step-by-step guide
Understand the default rules and why they must be changed
Understand the default rules and why they must be changed
When you create a Realtime Database, Firebase sets it to either test mode (all reads and writes allowed for 30 days) or locked mode (all reads and writes denied). Both are temporary starting points. Test mode rules expire after 30 days and lock your database completely. Locked mode blocks all client access. You must write custom rules before going to production. Open the Firebase Console, navigate to Realtime Database, and click the Rules tab to see your current rules.
1// DEFAULT TEST MODE (DANGEROUS - expires and allows all access)2{3 "rules": {4 ".read": true,5 ".write": true6 }7}89// DEFAULT LOCKED MODE (blocks everything)10{11 "rules": {12 ".read": false,13 ".write": false14 }15}Expected result: You can see the current rules in the Firebase Console Rules tab and understand that they need to be customized.
Require authentication for all reads and writes
Require authentication for all reads and writes
The most basic security rule requires users to be signed in before accessing any data. Use the auth variable, which is non-null when a user is authenticated with Firebase Auth. Set .read and .write to auth !== null at the root level. This blocks all unauthenticated access while allowing any signed-in user to read and write any data.
1{2 "rules": {3 ".read": "auth !== null",4 ".write": "auth !== null"5 }6}Expected result: Only authenticated users can read or write data. Unauthenticated requests are rejected.
Restrict users to their own data
Restrict users to their own data
For user-specific data, structure your database with user IDs as keys and write rules that match the auth.uid against the path variable. This ensures each user can only read and write their own data. Use the $uid wildcard variable to capture the user ID from the path and compare it against auth.uid.
1{2 "rules": {3 "users": {4 "$uid": {5 ".read": "auth !== null && auth.uid === $uid",6 ".write": "auth !== null && auth.uid === $uid"7 }8 },9 "posts": {10 "$postId": {11 ".read": "auth !== null",12 ".write": "auth !== null && (!data.exists() || data.child('authorId').val() === auth.uid)"13 }14 }15 }16}Expected result: Users can only read and write their own data under /users/{uid}. Posts can be read by any authenticated user but only written by the original author.
Validate data structure and types
Validate data structure and types
Use .validate rules to enforce data integrity. Validate that required fields exist, values have the correct type, strings meet length requirements, and numbers fall within expected ranges. Validate rules are checked on writes only and must pass for the write to succeed. Unlike .read and .write, .validate rules do not cascade down the tree, so you need to add them at each specific node.
1{2 "rules": {3 "users": {4 "$uid": {5 ".read": "auth !== null && auth.uid === $uid",6 ".write": "auth !== null && auth.uid === $uid",7 ".validate": "newData.hasChildren(['name', 'email'])",8 "name": {9 ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 100"10 },11 "email": {12 ".validate": "newData.isString() && newData.val().matches(/^[^@]+@[^@]+\.[^@]+$/)"13 },14 "age": {15 ".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 150"16 },17 "$other": {18 ".validate": false19 }20 }21 }22 }23}Expected result: Writes are rejected if required fields are missing, types are wrong, or values exceed the defined constraints.
Implement role-based access control
Implement role-based access control
Store user roles in the database (for example, under /roles/{uid}) and reference them in your rules using root.child(). This pattern lets you create admin, moderator, and regular user roles. Admins can read and write all data, while regular users have limited access. You can also use Firebase Auth custom claims for role-based access, which are available in auth.token.
1{2 "rules": {3 "roles": {4 "$uid": {5 ".read": "auth !== null && auth.uid === $uid",6 ".write": "auth !== null && root.child('roles/' + auth.uid + '/admin').val() === true"7 }8 },9 "content": {10 "$contentId": {11 ".read": "auth !== null",12 ".write": "auth !== null && (root.child('roles/' + auth.uid + '/admin').val() === true || root.child('roles/' + auth.uid + '/editor').val() === true)"13 }14 },15 "admin_panel": {16 ".read": "auth !== null && root.child('roles/' + auth.uid + '/admin').val() === true",17 ".write": "auth !== null && root.child('roles/' + auth.uid + '/admin').val() === true"18 }19 }20}Expected result: Access to different paths is controlled by the role stored in the /roles node. Only admins can access the admin_panel.
Add indexes for query performance
Add indexes for query performance
When you use orderByChild() queries on the client, add .indexOn rules at the parent path to create database indexes. Without indexes, the Realtime Database loads all child nodes and sorts them on the server, which is slow for large datasets. The .indexOn rule tells Firebase to maintain an index for the specified child keys.
1{2 "rules": {3 "posts": {4 ".indexOn": ["createdAt", "authorId"],5 "$postId": {6 ".read": "auth !== null",7 ".write": "auth !== null && auth.uid === newData.child('authorId').val()"8 }9 },10 "messages": {11 "$roomId": {12 ".indexOn": ["timestamp", "senderId"]13 }14 }15 }16}Expected result: Queries using orderByChild on the indexed fields run efficiently without loading all data into memory.
Test rules with the Rules Playground
Test rules with the Rules Playground
Before deploying, test your rules using the Rules Playground in the Firebase Console. Go to Realtime Database > Rules tab and click the Simulator button (or Rules Playground). Select the operation type (read, write, set), enter the path, optionally provide authenticated user details, and run the simulation. The playground shows whether the operation is allowed or denied and highlights which rule made the decision.
Expected result: You can simulate read and write operations against your rules to verify they work correctly before deploying.
Deploy rules from the CLI
Deploy rules from the CLI
Save your rules in a database.rules.json file in your project root and deploy them using the Firebase CLI. This lets you version control your rules alongside your application code. Make sure your firebase.json file points to the rules file. Deploy only the database rules without affecting other services using the --only flag.
1// In firebase.json, reference the rules file:2// {3// "database": {4// "rules": "database.rules.json"5// }6// }78// Deploy rules only:9// firebase deploy --only databaseExpected result: Rules are deployed from your local file to the production Realtime Database.
Complete working example
1{2 "rules": {3 "users": {4 "$uid": {5 ".read": "auth !== null && auth.uid === $uid",6 ".write": "auth !== null && auth.uid === $uid",7 ".validate": "newData.hasChildren(['name', 'email'])",8 "name": {9 ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 100"10 },11 "email": {12 ".validate": "newData.isString() && newData.val().matches(/^[^@]+@[^@]+\\.[^@]+$/)"13 },14 "role": {15 ".validate": "newData.isString() && (newData.val() === 'user' || newData.val() === 'editor' || newData.val() === 'admin')"16 },17 "$other": {18 ".validate": false19 }20 }21 },22 "posts": {23 ".indexOn": ["createdAt", "authorId"],24 "$postId": {25 ".read": "auth !== null",26 ".write": "auth !== null && (!data.exists() || data.child('authorId').val() === auth.uid || root.child('users/' + auth.uid + '/role').val() === 'admin')",27 ".validate": "newData.hasChildren(['title', 'content', 'authorId', 'createdAt'])",28 "title": {29 ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 200"30 },31 "content": {32 ".validate": "newData.isString() && newData.val().length > 0"33 },34 "authorId": {35 ".validate": "newData.isString() && newData.val() === auth.uid"36 },37 "createdAt": {38 ".validate": "newData.isNumber()"39 }40 }41 },42 "messages": {43 "$roomId": {44 ".indexOn": ["timestamp"],45 ".read": "auth !== null",46 "$messageId": {47 ".write": "auth !== null",48 ".validate": "newData.hasChildren(['text', 'senderId', 'timestamp'])",49 "text": {50 ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 1000"51 },52 "senderId": {53 ".validate": "newData.val() === auth.uid"54 },55 "timestamp": {56 ".validate": "newData.isNumber()"57 }58 }59 }60 }61 }62}Common mistakes when securing Firebase Realtime Database with Rules
Why it's a problem: Leaving test mode rules (read: true, write: true) in production
How to avoid: Replace test mode rules with proper authentication and authorization checks before launching. Set up a CI check that verifies rules require auth !== null at minimum.
Why it's a problem: Relying only on .write rules without .validate rules for data integrity
How to avoid: Always add .validate rules to enforce data types, required fields, and value constraints. A .write rule controls who can write, while .validate controls what they can write.
Why it's a problem: Not understanding that RTDB rules cascade: a .read: true at a parent grants read to all children
How to avoid: Rules cascade downward in the Realtime Database. Once read or write access is granted at a parent node, it cannot be revoked at a child node. Design your rules hierarchy carefully.
Why it's a problem: Querying a path without an .indexOn rule, causing slow full-scan queries
How to avoid: Add .indexOn at the parent path for any field you use with orderByChild(). Check the Firebase Console and function logs for UNINDEXED QUERY warnings.
Best practices
- Always require authentication (auth !== null) as the baseline for all read and write rules
- Use .validate rules to enforce data types, required fields, and constraints on every writable path
- Block unexpected fields with a $other wildcard set to .validate: false
- Store rules in database.rules.json under version control alongside your application code
- Test rules with the Rules Playground in the Firebase Console before deploying
- Add .indexOn for every field used in orderByChild queries to ensure efficient reads
- Use the cascading nature of rules intentionally: grant broad access at parent nodes, restrict at children where needed
- Review and update rules whenever your data model changes
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Help me write Firebase Realtime Database security rules that require authentication, restrict users to their own data, validate data structure and types, and implement role-based access control with admin, editor, and user roles.
Write a complete database.rules.json file for a Firebase Realtime Database that secures a users collection (owner-only access), a posts collection (authenticated read, author-only write, admin override), and a messages collection (authenticated read/write) with full data validation and indexing.
Frequently asked questions
What is the difference between .write and .validate rules?
The .write rule controls who can write to a path (authorization). The .validate rule controls what data can be written (data integrity). A write must pass both .write and .validate to succeed. Use .write for auth checks and .validate for data structure and type enforcement.
Do Realtime Database rules cascade like Firestore rules?
Yes, but differently. In the Realtime Database, once .read or .write is granted at a parent node, it applies to all child nodes and cannot be revoked at a deeper level. This is the opposite of Firestore, where rules do not cascade. Plan your RTDB rule hierarchy carefully.
How do I restrict a field to specific enum values?
Use the .validate rule with explicit value checks. For example, to allow only 'user', 'editor', or 'admin': .validate set to newData.isString() && (newData.val() === 'user' || newData.val() === 'editor' || newData.val() === 'admin').
Can I use custom claims in Realtime Database rules?
Yes. Firebase Auth custom claims are available in rules as auth.token.claimName. For example, to check for an admin claim: auth.token.admin === true. Custom claims are set via the Firebase Admin SDK.
How quickly do rule changes take effect?
Realtime Database rule changes typically take effect within a few seconds of deployment. There is no propagation delay like Firestore rules, which can take up to 10 minutes for active listeners.
What happens if I have no rules defined for a path?
If no .read or .write rule matches a path, access is denied by default. The Realtime Database is secure by default. You must explicitly grant access at each path or at a parent path.
Can RapidDev help audit and improve my Realtime Database security rules?
Yes. RapidDev can audit your existing rules for security vulnerabilities, optimize them for performance, and implement production-ready rules with authentication, role-based access, and data validation.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation