Why your first query returns nothing: policies are fail-closed
Published 2 min readAlso in Spanish
The most common surprise when an app first talks to Groveback: you create a collection, insert
a few documents from the dashboard, call find() from the SDK as a signed-in user — and get
nothing back.
Nothing is broken. The data plane is fail-closed: a collection with no policy denies every caller that is not an admin. The dashboard can see your documents because it uses admin routes; your app’s end users can’t, until a policy says they may.
The fix: say who owns what
Most collections start with “each user sees and edits their own documents”:
{
"list": { "filter": { "ownerId": "$auth.uid" } },
"read": { "filter": { "ownerId": "$auth.uid" } },
"create": { "validate": "true" },
"update": { "filter": { "ownerId": "$auth.uid" }, "immutableFields": ["ownerId"] },
"delete": { "filter": { "ownerId": "$auth.uid" } }
}
The filter is a Mongo query fragment $and-ed into the real query, so pagination and
indexes keep working. On create, you don’t need to send ownerId at all: the server
forces it to the caller’s id.
Three behaviors worth knowing
- A miss is a 404, not a 403. Reading a document you’re not allowed to see looks exactly like reading one that doesn’t exist. Existence is never revealed, so don’t build UI that branches on “forbidden” for single reads.
- An unresolvable placeholder denies. An anonymous caller hitting a rule with
$auth.uidgets nothing — it doesn’t match documents whoseownerIdhappens to be null. If you want public reads, write{ "filter": {} }forlist/readon purpose. PUTnever orphans a document.id,ownerIdandcreatedAtare re-pinned from the existing document, so a full replacement that forgetsownerIdkeeps the original owner.
Debug it before you ship it
The policy editor in the dashboard has a Test access simulator: pick an operation and a caller and see what the engine decides, without writing a line of client code. When a query comes back empty, that’s the first place to look.
More in the policies guide.