Skip to main content
Generaldavila7

PocketBase API Rules

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.

Stars
27,681
Source
davila7/claude-code-templates
Updated
2026-05-31
Slug
davila7--claude-code-templates--pb-api-rules
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/davila7/claude-code-templates/HEAD/cli-tool/components/skills/pocketbase/pb-api-rules/SKILL.md -o .claude/skills/pb-api-rules.md

Drops the SKILL.md into .claude/skills/pb-api-rules.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

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