API rules
Every base/auth collection has five rule slots: list_rule, view_rule,
create_rule, update_rule, delete_rule. View collections have only list_rule and view_rule. Each slot is one of:
null— public; anyone can perform this action.""(empty string) — admin only.- An expression evaluated against the record + auth context.
Admins always bypass expression rules.
Expression language
Section titled “Expression language”expr := operand op operand single comparison | expr "&&" expr logical AND | expr "||" expr logical OR | "(" expr ")" grouping
op := scalar | arrayscalar := "=" | "!=" | ">" | ">=" | "<" | "<=" | "~" | "!~"array := "?=" | "?!=" | "?>" | "?>=" | "?<" | "?<=" | "?~" | "?!~"
operand := literal | field | atRef | funcCall | macroliteral := "string" | 123 | true | false | nullfield := bareName ("." subPath)* (":" modifier)?atRef := "@request.auth." prop | "@request.method" | "@request.context" | ("@request.headers." | "@request.query." | "@request.body.") word | "@collection." name (":" alias)? "." path | datetimeMacromodifier := "isset" | "changed" | "length" | "each" | "lower"prop := id | email | typefuncCall := "geoDistance(" lonA "," latA "," lonB "," latB ")" | "strftime(" format "," time ("," modifier)* ")"Comparison operators
Section titled “Comparison operators”| Op | Meaning |
|---|---|
= != > >= < <= | Standard scalar compare. =/!= use loose equality with bool↔number coercion (SQL semantics). |
~ | Substring match (case-sensitive). |
!~ | NOT substring. |
Array-prefixed operators
Section titled “Array-prefixed operators”Prefix any scalar op with ? to switch to match-any-element semantics on
multi-value fields (multi-select, multi-relation, multi-file, JSON arrays):
status ?= "draft" at least one tag equals "draft"tags ?~ "feature" at least one tag contains "feature"contributors ?!= @request.auth.id at least one contributor isn't meField modifiers (:foo)
Section titled “Field modifiers (:foo)”Trailing :modifier on a field reference reshapes the operand:
| Modifier | Notes |
|---|---|
:isset | True when the request body submitted this field (independent of value). |
:changed | On update — true when the body’s value differs from the existing record. |
:length | Element count for multi-fields (file/select/relation arrays). |
:each | Lifts the comparison so it must hold for every element of an array field. |
:lower | Case-insensitive string compare. |
Request references
Section titled “Request references”| Reference | Notes |
|---|---|
@request.auth.id / .email / .type | Caller identity; id is "" unauth. type is "user" or "admin". |
@request.method | "GET" / "POST" / etc. |
@request.context | "default" / "oauth2" / "otp" / "password" / "realtime" / "protectedFile". |
@request.headers.<name> | Header by lowercased name (- becomes _). authorization and cookie are scrubbed before rule eval. |
@request.query.<name> | URL query param. |
@request.body.<name> | Submitted body field — useful for :isset/:changed. |
Cross-collection references
Section titled “Cross-collection references”@collection.<name>.<field> lookup by relation@collection.users:author.email alias the join (clarifies multi-hop joins)The collection’s own view_rule is inherited into the join — you can’t
escape access control by joining sideways.
Back-relations (<target>_via_<refField>)
Section titled “Back-relations (<target>_via_<refField>)”Reference the records that point at this record:
list_rule: comments_via_post:length > 0…matches posts with at least one comment whose post relation references
the row.
Datetime macros
Section titled “Datetime macros”@now, @yesterday, @tomorrow, @todayStart, @todayEnd, @monthStart,
@monthEnd, @yearStart, @yearEnd, plus @second, @minute, @hour,
@day, @weekday, @month, @year for the current time’s components.
list_rule: published_at <= @now && published_at >= @monthStartFilter functions
Section titled “Filter functions”| Function | Use |
|---|---|
geoDistance(lonA, latA, lonB, latB) | Distance in km between two points; pair with comparisons for radius search. |
strftime(format, time, mods…) | SQLite-compatible date formatting in filters. |
How rules apply
Section titled “How rules apply”| Slot | When evaluated | Against |
|---|---|---|
list_rule | GET /api/<col> | Compiled into SQL filter — applied as a WHERE so non-matching rows never load. |
view_rule | GET /api/<col>/:id | The fetched record. |
create_rule | POST /api/<col> | The incoming body. |
update_rule | PATCH /api/<col>/:id | The existing record (pre-update). |
delete_rule | DELETE /api/<col>/:id | The existing record. |
Failure → 403 Forbidden.
Examples
Section titled “Examples”Public read, admin write
Section titled “Public read, admin write”list_rule: null (public)view_rule: null (public)create_rule: "" (admin only — empty string)update_rule: ""delete_rule: ""Owner-only read
Section titled “Owner-only read”Records have an author relation field. Logged-in users see only their own:
list_rule: @request.auth.id = authorview_rule: @request.auth.id = authorThe list_rule becomes WHERE author = ? with the user’s id bound at query
time. Non-matching rows never reach the application.
Authenticated create, owner update/delete
Section titled “Authenticated create, owner update/delete”create_rule: @request.auth.id != ""update_rule: @request.auth.id = authordelete_rule: @request.auth.id = authorPublished-or-mine
Section titled “Published-or-mine”Show published posts to everyone; let authors see their drafts too:
list_rule: published = true || @request.auth.id = authorAdmins only for everything
Section titled “Admins only for everything”list_rule: ""view_rule: ""create_rule: ""update_rule: ""delete_rule: ""Block writes that mutate a forbidden field
Section titled “Block writes that mutate a forbidden field”update_rule: @request.body.role:isset = false || @request.auth.type = "admin"Non-admins cannot send a role in the body. Admins can.
Restrict updates to fields that didn’t change
Section titled “Restrict updates to fields that didn’t change”update_rule: status:changed = false || @request.auth.type = "admin"Tag-based ACL
Section titled “Tag-based ACL”Posts have tags: select[]. Show posts visible to any of the user’s tag list:
list_rule: tags ?= @request.auth.id(?= matches when at least one element of tags equals the user id.)
Geo radius
Section titled “Geo radius”Locations have a point: geoPoint. Filter by distance:
?filter=geoDistance(point.lng, point.lat, -73.99, 40.74) < 5Field references
Section titled “Field references”A bare name in an expression refers to the record’s column. Special aliases:
id→ record idcreated/created_at→ created_at columnupdated/updated_at→ updated_at column
Unknown fields evaluate to undefined — comparisons with undefined are
“equal to empty string / null” via the loose-equality rule.
Authoring rules in the admin
Section titled “Authoring rules in the admin”The schema editor includes a typed autocomplete for rules — start typing
@request. and the popup lists auth.id, auth.email, auth.type. Bare
names autocomplete against the collection’s actual fields. Operators are
suggested on Tab / Enter.
Logging rule outcomes
Section titled “Logging rule outcomes”Every records-API request records which rules evaluated and what they returned. The admin Logs page surfaces:
- Rule name (
list_rule,view_rule, etc.) and collection - Expression text (or
(public)/(admin only)) - Outcome:
allow,deny, orfilter(list rule applied as SQL filter) - Reason:
public,admin only,admin bypass,rule passed,rule failed,applied as SQL filter
Invaluable for debugging “why am I getting 403 on this collection”.