Reference
Policy rules that stay close to the data.
Policies are JSON documents. The engine compiles them into parameterized SQL and attaches the predicate to every request; a table with no document refuses every caller.
Claims and operators
| Expression | Meaning | Notes |
|---|---|---|
$auth.uid | The verified user id, from the JWT | Bound as a SQL parameter. Absent means the anon role. |
$auth.email | The verified email | Bound as a SQL parameter. |
$auth.app.* | Server-set metadata | Only operators and your server can write it, so policies may trust it. |
$auth.user.* | User-editable metadata | Refused when the document is saved. End users can write it themselves, so a policy reading it would be self-service escalation. |
_eq _neq _gt _gte _lt _lte _like _is_null | Comparisons | Every value is a bound parameter, never spliced into SQL. |
_in _and _or _not _exists | Composition | _in compiles to json_each so a whole list costs one parameter; _exists is a correlated subquery for reaching related tables. |
Example
{
"table": "posts",
"enabled": true,
"binds": {
"isPublished": { "status": { "_eq": "published" } },
"isAuthor": { "author_id": { "_eq": "$auth.uid" } }
},
"policies": [
{
"name": "read_own_or_published",
"for": "select",
"to": ["authenticated"],
"using": { "_or": [{ "$bind": "isPublished" }, { "$bind": "isAuthor" }] },
"columns": ["id", "title", "body", "status", "author_id", "created_at"]
}
]
}select "posts"."id", "posts"."title", "posts"."body",
"posts"."status", "posts"."author_id", "posts"."created_at"
from "posts"
where (("posts"."status" = ?) or ("posts"."author_id" = ?))Refusal behavior
A table with no document answers 404 for every caller. A denied read simply lacks the rows. A write that matches nothing answers 404 whether the row was absent or the policy withheld it: the two cases are deliberately indistinguishable, because a caller who could tell them apart could walk a range of ids and learn which exist. Writes compile to single guarded statements, so there is no partial state to clean up.
D1 bills rows scanned, not rows returned, so a policy column without an index is a recurring bill rather than a latency problem. baseclf policy lint names the columns and hands back the CREATE INDEX to paste, and every response carries an x-baseclf-rows-read header so you can watch it.