Security Rules
NukeBase uses a JSON-based security rules system to control access to your database. Rules are defined in
server/rules.js and are evaluated for every database operation.
Available Variables in Rules:
admin- The standard auth context for the caller (admin.uid,admin.claims, etc.) — see Auth Context for the full shaperoot- The database object at the top leveldata- The current/old value at the path being accessednewData- The new value being written (for write/validate rules)$variables- Wildcard captures like$userId,$postId
Rule Types
Three types of rules control different aspects of data access:
- read - Controls who can read data at a path (triggered by
get()operations) - write - Controls who can create, update, or delete data (triggered by
set(),update(), andremove()operations) - validate - Ensures data meets specific requirements (triggered by
set()andupdate()operations)
How Rules Are Checked:
Read and write rules grant access — they do not revoke it. When you read or write at a path like users.john.email, NukeBase walks from the root toward that path and evaluates each level that has a matching rule:
- Check
users— if its rule returnstrue, ALLOWED (stop here) - Check
users.john— if its rule returnstrue, ALLOWED (stop here) - Check
users.john.email— if its rule returnstrue, ALLOWED
Any single level returning true grants access. The operation is denied only if no level along the path grants. A "read": "false" at a parent does NOT prevent a child rule from granting access at a deeper path — it just means that level didn't grant on its own.
Validate rules behave differently. They cascade through every level along the write path AND into the new value, and ALL applicable rules must pass. Any single failure denies the write.
Rule Matching at Same Level:
- Read/Write rules: If you have both exact (
pets) and wildcard ($other) rules at the same level, BOTH must pass for access topets. - Validate rules: Only the most specific rule matches. Exact match (
pets) takes priority over wildcard ($other).
// These two rules are at the SAME LEVEL (both are direct children of the parent)
module.exports = {
"pets": {
"read": "true", // Rule 1: Anyone can read pets
"write": "admin.claims.role == 'petOwner'", // Rule 2: Must be pet owner
"validate": "newData.type == 'cat' || newData.type == 'dog'" // Only cats/dogs
},
"$other": { // ← This is at the SAME LEVEL as "pets" above
"read": "admin.claims.role == 'admin'", // Rule 3: Must be admin
"write": "false", // Rule 4: No writes allowed
"validate": "newData != null" // Not empty
}
}
// When accessing "pets":
// Read: BOTH "true" AND "admin.claims.role == 'admin'" must pass → Fails for non-admins!
// Write: BOTH "admin.claims.role == 'petOwner'" AND "false" must pass → Always fails!
// Validate: ONLY the "pets" rule applies (most specific)
Basic Example
module.exports = {
"users": {
"$userId": {
// Don't grant a blanket read at $userId — that grant cascades down
// and would override the deeper email rule. Grant read on the
// public-facing fields instead.
"write": "admin.uid == $userId", // Only the user can edit their profile
"name": { "read": "true" }, // Public
"bio": { "read": "true" }, // Public
"email": { "read": "admin.uid == $userId" } // Private — only the user
}
}
};
Path Patterns
Rules support different path patterns to match your data structure:
| Pattern | Description | Example |
|---|---|---|
users.john |
Exact path matching | Matches only users.john |
users.$userId |
Wildcard matching | Matches users.alice, users.bob, etc.The $userId variable captures the actual key |
posts.$postId |
Wildcard for collections | Matches any child: posts.abc, posts.xyz, etc. |
messages.$msgId |
Works with arrays too | Arrays are objects with numeric keys Matches messages.0, messages.1, messages.2 |
Arrays and Path Matching:
JavaScript arrays like ["red", "blue", "green"] are stored as objects with numeric keys:
{ "0": "red", "1": "blue", "2": "green" }
This means:
colors.0- Exact match for first elementcolors.$index- Wildcard matches all elements (0, 1, 2, etc.)colors- Matches the array itself
Operations and Their Rules
Different database operations trigger different combinations of rules:
| Operation | Rules Triggered | Description |
|---|---|---|
get() |
read | Only read rules are checked when retrieving data |
set() |
write + validate | Both write permission and data validation are required |
update() |
write + validate | Same as set() - must have permission and valid data |
remove() |
write | Only write rules are checked (newData is null) |
query() |
read | Read rules filter which items are returned |
Rule Evaluation by Path Depth
The set of rules that actually applies to a given operation is determined dynamically by the depth of the path you're targeting. Two writes against the same rules file can hit completely different rules depending on how deep the operation lands. Designing security correctly means knowing exactly which rules will be evaluated for each call.
Read / Write — walked from root toward the target path
NukeBase iterates each level along the path and evaluates any matching rule. If any one level returns true, access is granted and evaluation stops. Deeper rules are not consulted past a grant. The operation is denied only if no level along the path grants.
Validate — cascades through every level and into the new value
Validate runs at every prefix of the write path AND at every leaf inside the new value. All applicable validate rules must pass; a single failure denies the write.
Given the rules below, here's what gets evaluated for writes at different depths:
module.exports = {
"store": {
"write": "admin.claims.role == 'admin'",
"products": {
"write": "admin.claims.role == 'manager'",
"$productId": {
"write": "admin.uid == data.ownerId",
"validate": "newData.name && newData.price > 0"
}
}
}
};
| Operation | Rules evaluated | Outcome |
|---|---|---|
set(["store"], {...}) |
Write: store.write onlyValidate: any validate rule reachable from the new value (e.g. store.products.$productId.validate for each product in the payload) |
Allowed only if admin. Note: only the top-level write rule is checked — the deeper write rules are not consulted, because the operation targets ["store"]. |
set(["store","products"], {...}) |
Write: store.write, then store.products.writeValidate: store.products.$productId.validate for each product leaf in the payload |
Allowed if the caller is admin OR a manager (any one returning true grants). Validate must also pass for every product written. |
set(["store","products","abc"], {name:"X", price:5}) |
Write: store.write, store.products.write, store.products.$productId.writeValidate: store.products.$productId.validate |
Allowed if admin OR manager OR the caller owns "abc". Validate runs against the new value. |
get(["store","products","abc"]) |
Read: store.read, store.products.read, store.products.$productId.read (none are defined here, so the call is denied) |
Denied — no level along the path grants read. |
Common pitfall — a blanket grant at a parent cascades. Writing "users.$userId.read": "true" means any deeper read rule like "users.$userId.email.read": "admin.uid == $userId" is effectively bypassed: the parent's true grants access first, and the email rule never runs. To restrict deeper data, don't grant blanket access at the parent — split the data into subnodes (e.g. public / private) and grant read only on the part you want exposed.
Mental model: read/write rules answer the question "is there any reason to allow this?" — one yes is enough. Validate rules answer "does the new data satisfy every constraint?" — one no is enough.
Rule Types in Detail
Read Rules
Control who can read data at a specific path:
// Simple read rule
// Don't put "read": "true" at the $postId level — it would cascade and
// override the draft restriction. Grant read on the published fields only.
"posts": {
"$postId": {
"title": { "read": "true" },
"body": { "read": "true" },
"draft": { "read": "admin.uid == data.authorId" } // Only author can read drafts
}
}
// Using variables in paths
"users": {
"$userId": {
"name": { "read": "true" }, // Public
"email": { "read": "admin.uid == $userId" } // Only the user can read their own email
}
}
Write Rules
Control who can create, update, or delete data:
// Basic write rule
"posts": {
"$postId": {
"write": "admin.uid == data.authorId", // Only author can edit
"createdAt": {
"write": "!data" // Can only set createdAt when creating (no previous data)
}
}
}
// How write rules cascade along the target path
// (any rule along the path that returns true is sufficient)
"store": {
"write": "false", // Blocks writes that TARGET ["store"] directly
"products": {
"write": "admin.claims.role == 'manager'", // Applies when writing AT ["store","products"] or deeper
"$productId": {
"write": "admin.uid == data.ownerId" // Applies when writing AT ["store","products",]
}
}
}
// What actually happens:
// set(["store"], ...) → only store.write applies → DENIED
// set(["store","products"], ...) → store.write OR store.products.write
// → ALLOWED if user is a manager
// set(["store","products","abc"], ...) → store.write OR store.products.write
// OR store.products.$productId.write
// → ALLOWED if manager OR uid == data.ownerId
Validate Rules
Ensure data integrity and format requirements:
// Simple field validation
"users": {
"$userId": {
"age": {
"validate": "newData >= 13 && newData <= 120"
},
"email": {
"validate": "newData.includes('@') && newData.includes('.')"
}
}
}
// Validating objects with required fields
"posts": {
"$postId": {
"validate": "newData.title && newData.content && newData.title.length <= 200"
}
}
// Using data and newData to compare old and new values
"users": {
"$userId": {
"credits": {
// Ensure credits can only increase, not decrease
"validate": "newData >= data"
}
}
}
// Complex validation with multiple conditions
"products": {
"$productId": {
"validate": "newData.name && newData.price > 0 && newData.stock >= 0"
}
}
Array Validation
Arrays are validated using the same rule system, but understanding how paths are generated is essential for proper validation.
How Array Validation Works:
When you set/update an array, NukeBase generates validation paths for:
- The array itself - Path to the array as a whole
- Each array element - Individual paths like
["tags", "0"],["tags", "1"]
Arrays are treated as objects with numeric keys: ["red", "blue"] becomes {"0": "red", "1": "blue"}
// Example: update(["users", "john", "tags"], ["red", "blue", "green"])
// This generates paths:
// 1. ["users", "john", "tags"] ← Entire array
// 2. ["users", "john", "tags", "0"] ← Element 0: "red"
// 3. ["users", "john", "tags", "1"] ← Element 1: "blue"
// 4. ["users", "john", "tags", "2"] ← Element 2: "green"
// METHOD 1: Validate the ENTIRE array
"users": {
"$userId": {
"tags": {
// newData = entire array ["red", "blue", "green"]
"validate": "Array.isArray(newData) && newData.length <= 5"
}
}
}
// METHOD 2: Validate EACH element using wildcard
"users": {
"$userId": {
"tags": {
"$index": { // $index matches "0", "1", "2", etc.
// newData = individual element ("red", "blue", or "green")
"validate": "typeof newData === 'string' && newData.length < 20"
}
}
}
}
// METHOD 3: COMBINE both approaches
"users": {
"$userId": {
"tags": {
// Validate array properties
"validate": "Array.isArray(newData) && newData.length <= 5",
"$index": {
// Validate each element
"validate": "typeof newData === 'string' && newData.length < 20"
}
}
}
}
// Complex array validation with element uniqueness check
"users": {
"$userId": {
"favoriteColors": {
"$index": {
// Each color must be a valid hex code
"validate": "typeof newData === 'string' && /^#[0-9A-F]{6}$/i.test(newData)"
}
}
}
}
Important: Both the array-level rule AND element-level rules must pass. If you have rules at both levels, all of them are checked.
Available Variables
Rules have access to several context variables:
| Variable | Description | Available In |
|---|---|---|
data |
Current value at the path (before changes) | All rule types |
newData |
Value after the write operation | write, validate |
root |
Current database root | All rule types |
admin |
Auth context (see Auth Context) | All rule types |
$variables |
Values from wildcard path segments | All rule types |
Best Practices
- Start with restrictive rules, then add exceptions as needed
- Use validate rules to ensure data integrity
- Test rules thoroughly before deploying to production
- Keep rules simple and readable
- Only one validate rule per path - combine conditions with
&&or|| - Read/write rules grant access — any single rule along the path that returns
trueis sufficient. Don't put a blanket"read": "true"at a parent if you intend to restrict child paths; the parent grant cascades and the deeper rule never gets the chance to deny. - Validate rules only match the most specific rule at a given path
Common Mistakes to Avoid
Mistake 1: Multiple validate rules on same path
// WRONG - Only the last validate rule will be used!
"email": {
"validate": "newData.includes('@')",
"validate": "newData.includes('.')" // This overwrites the first rule!
}
// CORRECT - Combine with &&
"email": {
"validate": "newData.includes('@') && newData.includes('.')"
}