PocketBase auth rules

Every collection in TinyCld needs auth rules. Without them, PocketBase falls back to “superusers only” and every insert, list, view, update, and delete from a non-superuser session fails with:

Only superusers can perform this action.

You add rules at collection-creation time inside your pb-migrations/<timestamp>_create_<thing>.js file.

The five rules

PocketBase collections accept five auth rules:

RuleWhen it firesDefault if omitted
listRulefiltered list / searchsuperuser-only
viewRulesingle-record fetch by idsuperuser-only
createRulerecord creationsuperuser-only
updateRulerecord updatesuperuser-only
deleteRulerecord deletesuperuser-only

Each rule is a string in PocketBase’s filter language. It evaluates against the record being accessed plus the special @request.auth and @request.data namespaces. When the expression is truthy, the action is allowed.

Owned by one user (most common)

A deployment is one workspace, so there is no organization to scope by: a private record carries an owner relation straight to the users collection (id _pb_users_auth_), and the rule is “the caller is the owner”. Always AND it with the disabled check — core blocks a suspended account from getting a token, but PocketBase evaluates these rules for a token that already exists, so the rule is the whole authorization:

const enabled = '@request.auth.disabled != true'
const isOwner = 'owner = @request.auth.id'

new Collection({
    type: 'base',
    name: 'todo_items',
    listRule: `${enabled} && ${isOwner}`,
    viewRule: `${enabled} && ${isOwner}`,
    createRule: `${enabled} && ${isOwner}`,
    updateRule: `${enabled} && ${isOwner}`,
    deleteRule: `${enabled} && ${isOwner}`,
    fields: [
        { name: 'name', type: 'text', required: true, max: 200 },
        {
            name: 'owner',
            type: 'relation',
            required: true,
            collectionId: '_pb_users_auth_',
            cascadeDelete: true,
            maxSelect: 1,
        },
        // ...
    ],
})

This is exactly what @tinycld/contacts ships.

Shared by members with roles

Calendars, boards, and shared mailboxes keep a membership collection (calendar_members: calendar, user, role) and let the rule walk it with PocketBase’s back-relation syntax. Name the roles that may write rather than excluding one — ?!= "viewer" silently grants write to every role added later:

const enabled  = '@request.auth.disabled != true'
const authed   = '@request.auth.id != ""'
const notGuest = '@request.auth.role != "guest"'

const isMember  = 'calendar_members_via_calendar.user ?= @request.auth.id'
const isOwner   = `${isMember} && calendar_members_via_calendar.role ?= "owner"`
const viaWriter = 'calendar.calendar_members_via_calendar.user ?= @request.auth.id && ' +
                  '(calendar.calendar_members_via_calendar.role ?= "owner" || ' +
                  'calendar.calendar_members_via_calendar.role ?= "editor")'

calendars.listRule   = `${enabled} && ${isMember}`
calendars.createRule = `${authed} && ${notGuest} && ${enabled}`
calendars.updateRule = `${enabled} && ${isOwner}`
events.createRule    = `${enabled} && ${viaWriter}`

A user’s workspace role (owner, admin, member, guest) lives on the users record itself, so a rule reads it directly: @request.auth.role != "guest". Anything a rule cannot express because it sees only one row — “don’t remove the last owner” — stays a Go hook, with the rule as defence in depth.

Public read, owner write

Public share-link content, blog posts, anything where read is open but writes are gated:

listRule: '',                          // empty string = anyone
viewRule: '',
createRule: 'owner = @request.auth.id',
updateRule: 'owner = @request.auth.id',
deleteRule: 'owner = @request.auth.id',

Empty string '' means the rule allows everyone. null (or omitting the rule) means superusers only — which is rarely what you want at the API level.

Locked down

If a collection is only ever written by Go server hooks (audit logs, system events), set every rule to null:

listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,

The Go side bypasses rules with app.Save(record) calls in hooks; null rules at the API level enforce that no client can write directly.

Common patterns

Where to find existing examples

Every present feature package’s pb-migrations/ is a working reference:

The @tinycld/bootstrap scaffolder writes a starter migration that already includes the owner-scoped rules and an owner relation to users; rename or remove the field as your data model evolves.

Diagnostics

If you see “Only superusers can perform this action” at runtime, the rule for the action you tried (insert → createRule, list → listRule, etc.) is null or missing. Run pnpm run db:reset from tinycld/ after editing the migration so the new rules take effect — PocketBase doesn’t hot-reload rule changes from a previously-applied migration.