Rules
A rule is a filter expression that can reference the caller. It's compiled into the SQL that reads or writes the rows, so rows a user can't see never leave SQLite.
Where rules go
Each collection has five rules, one per action. A missing rule means superusers only; "" means anyone.
"rules": {
"list": "@request.auth.id != null",
"view": "@request.auth.id != null",
"create": "author = @request.auth.id",
"update": "author = @request.auth.id",
"delete": "author = @request.auth.id || @request.auth.staff = true"
}Fields can have two rules of their own: visible (who can read it) and writable (who can set it). Both default to the collection’s rules.
{ "name": "mark", "type": "number",
"visible": "@request.auth.id = pupil || @request.auth.staff = true",
"writable": "@request.auth.roles ~ \"examiner\"" }The language
Same syntax as filters:
| Compare | = != > >= < <= |
| Contains / doesn’t contain | ~ !~ |
| Combine | && || and parentheses |
| Values | "text", numbers, true, false, null |
| The record | field names: author, org, status |
| The caller | @request.auth.… |
| Time | @now, @days_ago.30, @years_ago.13 |
The caller
@request.auth.id,collection,emailandsuperusercome from the token and are free.- Any other field of the caller’s record works too:
@request.auth.staff,@request.auth.org. The record is loaded by id, in the same transaction, only when a rule uses it. @request.auth.rolesis a set:@request.auth.roles ~ "admin"tests membership, so it never matches “superadmin”.- For anonymous callers all of these are
null, so a members-only rule just evaluates to false, never an error.
@request.auth.staff = true && org = @request.auth.org && id != @request.auth.idStaff can delete users in their own organisation, but not themselves.
How they are enforced
- Lists filter. Callers get only rows they may see, with correct totals.
- View, update and delete return 404 when denied, so rules can’t be used to probe whether a row exists.
- Writes are checked twice: before the change and again inside the transaction after it. A change that would put the row out of the writer’s own reach is rolled back.
- Caller-only conditions are resolved before the query.
@request.auth.roles ~ "admin"becomes a constant; conditions on the record go into theWHEREclause. - Field rules work the same way: a field the caller can never see isn’t selected; one that depends on the row is nulled per row, in SQL.
Checking permissions up front
GET /api/acl returns, for the signed-in user, what each collection allows: allow, deny, or conditional (only for some rows, e.g. their own). Use it to show only buttons that will work. It evaluates the same rules, so it can’t disagree with enforcement.