NukeBase

Query Authorization

A query is checked once, before it runs, against the read rules along path + the wildcard child level + childPath. Understanding that one sentence prevents two mistakes.

Read rules do not filter query results. There is a single permission check for the whole query. If it passes, every matching record is returned; if it fails, the query returns status: "Failed" and nothing else. Rules never run per record, so you cannot rely on them to strip private rows out of a result set — use childPath to scope what the query can reach, or keep private data in a separate subtree.

A read rule that compares against a wildcard will deny queries. The check walks a synthetic path with a $ placeholder standing in for "any child", so a wildcard variable binds to the literal string "$" rather than a real key. Given:

"users": { "$uid": { "read": "admin.uid == $uid" } }

a get(["users","matt"]) succeeds for Matt, but query({ path: ["users"], … }) is denied for everyone — the rule evaluates admin.uid == "$". To make a collection queryable, grant read with a rule that does not depend on the wildcard's value:

"users": {
  "$uid": {
    "public":  { "read": "true" },                     // queryable with childPath: ["public"]
    "private": { "read": "admin.uid == $uid" }         // get-only, never queryable
  }
}

The practical consequence: scope by path, not by filter. Put each user's data somewhere they can be granted, and query inside it — rather than querying a shared collection and hoping rules narrow the result.

Scoping a query per user
// WRONG — a shared collection with an ownership rule. The collection check
// runs once with $orderId unbound, so this returns nothing at all.
//   rules: { "orders": { "$orderId": { "read": "admin.uid == data.ownerId" } } }
query({ path: ["orders"], query: "child.status == 'open'" });

// RIGHT — give each user their own subtree, grant that, and query inside it.
//   rules: { "orders": { "$uid": { "read": "admin.uid == $uid" } } }
query({ path: ["orders", myUid], query: "child.status == 'open'" });

// Also fine — a genuinely public collection.
//   rules: { "posts": { "$postId": { "read": "true" } } }
query({ path: ["posts"], query: "child.published == true" });

Server-side query() runs as root and bypasses all of this, so a callable or a POST endpoint is the right place to implement a filtered read that rules cannot express.