API rules and filter expressions for PocketBase access control. Use when setting permissions, writing filter expressions, configuring who can access what, or debugging 403/404 responses. Covers all 5 rule types, filter syntax, operators, request/collection macros, and field modifiers.
Each collection has 5 rule types. Each rule is a filter expression that must evaluate to true for the request to proceed.
| Rule | Controls | Locked = | Empty string = |
|---|---|---|---|
| List | GET /api/collections/{name}/records | superusers only | everyone can list |
| View | GET /api/collections/{name}/records/{id} | superusers only | everyone can view |
| Create | POST /api/collections/{name}/records | superusers only | everyone can create |
| Update | PATCH /api/collections/{name}/records/{id} | superusers only |
| everyone can update |
| Delete | DELETE /api/collections/{name}/records/{id} | superusers only | everyone can delete |
Critical: null/locked means only superusers can perform the action (regular users and guests are denied). Empty string "" means EVERYONE including guests. Superusers always bypass API rules entirely — see below.
Superusers (formerly admins) always bypass API rules. Rules only apply to regular auth records and guests.
| Operator | Meaning | Example |
|---|---|---|
= | Equal | status = "active" |
!= | Not equal | status != "draft" |
> | Greater than | count > 5 |
>= | Greater or equal | count >= 5 |
< | Less than | count < 10 |
<= | Less or equal | count <= 10 |
~ | LIKE (contains) | title ~ "hello" |
!~ | NOT LIKE | title !~ "spam" |
?= | Any/has (array contains) | tags ?= "TAG_ID" |
?!= | None (array not contains) | tags ?!= "TAG_ID" |
?> | Any greater than | scores ?> 90 |
?>= | Any greater or equal | scores ?>= 90 |
?< | Any less than | scores ?< 10 |
?<= | Any less or equal | scores ?<= 10 |
?~ | Any LIKE | emails ?~ "@gmail.com" |
?!~ | Any NOT LIKE | emails ?!~ "@test.com" |
Critical: use ?= (not =) for multi-valued fields (multi-select, multi-relation, multi-file). = checks the raw JSON string, ?= checks individual values.
status = "active" && author = @request.auth.id
status = "active" || status = "featured"
Parentheses for grouping: (a = 1 || b = 2) && c = 3
"value" or 'value'123, 45.67true, falsenull — empty/missing value@request.*)Access the current request context in rules:
| Macro | Type | Description |
|---|---|---|
@request.auth.id | string | Current auth record ID (empty if guest) |
@request.auth.email | string | Current auth record email |
@request.auth.verified | bool | Whether email is verified |
@request.auth.collectionId | string | Auth collection ID |
@request.auth.collectionName | string | Auth collection name |
@request.auth.* | any | Any field from the auth record |
@request.body.fieldName | any | Field value from request body |
@request.query.paramName | string | URL query parameter |
@request.headers.name | string | Request header (lowercase key) |
@request.method | string | HTTP method (GET/POST/PATCH/DELETE) |
You can traverse relations on the auth record:
@request.auth.team.owner = @request.auth.id
@collection.*)Cross-collection lookups without explicit joins:
@collection.memberships.user ?= @request.auth.id &&
@collection.memberships.team ?= team
This checks if a record exists in the memberships collection where the user matches the current auth user and the team matches the current record's team field.
Note: @collection.* performs an implicit EXISTS subquery. It's powerful but can be slow on large datasets — add indexes.
Use in create/update rules to validate specific field behaviors:
| Modifier | Works on | Description |
|---|---|---|
:isset | @request.body.* | True if the field was sent in the request (even if empty) |
:changed | record field | True if the field value differs from current stored value (update only) |
:length | string/array | Returns the length |
:each | array | Applies the condition to each element |
:lower | string | Lowercased value |
// Only allow changing status if user is owner