Claude-code-templates 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.

install
source · Clone the upstream repo
git clone https://github.com/davila7/claude-code-templates
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/davila7/claude-code-templates "$T" && mkdir -p ~/.claude/skills && cp -r "$T/cli-tool/components/skills/pocketbase/pb-api-rules" ~/.claude/skills/davila7-claude-code-templates-pocketbase-api-rules && rm -rf "$T"
manifest: cli-tool/components/skills/pocketbase/pb-api-rules/SKILL.md
source content

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.

RuleControlsLocked =Empty string =
List
GET /api/collections/{name}/records
superusers onlyeveryone can list
View
GET /api/collections/{name}/records/{id}
superusers onlyeveryone can view
Create
POST /api/collections/{name}/records
superusers onlyeveryone can create
Update
PATCH /api/collections/{name}/records/{id}
superusers onlyeveryone can update
Delete
DELETE /api/collections/{name}/records/{id}
superusers onlyeveryone 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

OperatorMeaningExample
=
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:

MacroTypeDescription
@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:

ModifierWorks onDescription
:isset
@request.body.*
True if the field was sent in the request (even if empty)
:changed
record fieldTrue 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

MacroExample 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