--- name: "PocketBase API Rules" description: "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." --- # 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 ```