PocketBase API Rules & Filter Expressions
Rule Types
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.
Superuser Bypass
Superusers (formerly admins) always bypass API rules. Rules only apply to regular auth records and guests.
Filter Syntax
Operators
| 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.
Logical Operators
status = "active" && author = @request.auth.id
status = "active" || status = "featured"
Parentheses for grouping: (a = 1 || b = 2) && c = 3
Values
- Strings:
"value"or'value' - Numbers:
123,45.67 - Booleans:
true,false null— empty/missing value- Identifiers: field names, macros
Request Macros (@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) |
Auth record relations
You can traverse relations on the auth record:
@request.auth.team.owner = @request.auth.id
Collection Macros (@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.
Field Modifiers
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 |
Examples
// Only allow changing status if user is owner
status:changed = false || author = @request.auth.id
// Prevent setting role on create
@request.body.role:isset = false
// Require at least 2 tags
@request.body.tags:length >= 2
// Check each tag is from allowed list
@request.body.tags:each ?= @collection.allowed_tags.id
Datetime Macros
| Macro | Example output |
|---|---|
@now |
2024-01-15 10:30:00.000Z |
@second |
2024-01-15 10:30:00.000Z |
@minute |
2024-01-15 10:30:00.000Z |
@hour |
2024-01-15 10:00:00.000Z |
@day |
2024-01-15 00:00:00.000Z |
@month |
2024-01-01 00:00:00.000Z |
@year |
2024-01-01 00:00:00.000Z |
@todayStart |
2024-01-15 00:00:00.000Z |
@todayEnd |
2024-01-15 23:59:59.999Z |
@monthStart |
2024-01-01 00:00:00.000Z |
@monthEnd |
2024-01-31 23:59:59.999Z |
@yearStart |
2024-01-01 00:00:00.000Z |
@yearEnd |
2024-12-31 23:59:59.999Z |
Arithmetic: @now - 7d, @now + 1h, @now - 30m
geoDistance()
For location-based filtering:
geoDistance(lat, lon, 40.7128, -74.0060) <= 10000
Arguments: geoDistance(latField, lonField, targetLat, targetLon) — returns meters.
Common Patterns
Owner-only access
// View/Update/Delete rule:
author = @request.auth.id
Authenticated users only
@request.auth.id != ""
Verified users only
@request.auth.verified = true
Role-based access
@request.auth.role = "admin" || author = @request.auth.id
Team membership
@collection.team_members.user ?= @request.auth.id &&
@collection.team_members.team ?= team
Public read, owner write
// List/View: "" (empty = everyone)
// Create: @request.auth.id != ""
// Update/Delete: author = @request.auth.id
Prevent field modification
// Update rule: prevent changing `owner` after creation
owner:changed = false
Time-limited access
expires > @now