Skip to content

Roles & permissions

Permissions are the coarse gate: may this caller touch this collection and operation at all? Policies are the fine gate: which documents? A request must pass both.

Three shapes:

Shape Example Meaning
data:<collection>:<op> data:posts:read A data-plane operation. Ops are read, list, create, update, delete, admin.
domain:action functions:manage An administrative capability.
* * Everything.

Common domain permissions: collections:manage, policies:manage, roles:manage, users:manage, apikeys:manage, functions:manage, functions:invoke, storage:manage, storage:read, storage:write, webhooks:manage, integrations:manage, audit:read, config:manage, auth:config:read, auth:config:write.

They work per segment. All three of these satisfy data:posts:read:

data:*
data:*:read
data:posts:*

Permission checks are fail-closed, which is why an invalid permission string would silently grant nothing — so role writes validate them up front and reject bad ones.

Member roles End-user roles
Who holds them Developers administering the project Your app’s users
Assigned via Project membership A user’s metadata.role
Built-ins Owner, Admin, Editor, Viewer user
Vocabulary Any permission string A restricted catalog (below)
API /api/v1/admin/roles /api/v1/admin/enduser-roles
Bundle file roles.json enduser-roles.json

Both are gated on the same roles:manage permission, held by Owner and Admin. Member role names are rejected as end-user role names, to keep the two visually distinct.

{
"name": "Content Editor",
"permissions": ["data:*:read", "data:posts:update", "collections:manage"]
}

Names must start with a letter, be at most 64 characters, and contain only letters, digits, spaces, _ and -.

The four built-ins — Owner, Admin, Editor, Viewer — are listed but cannot be created, edited or deleted; their names are reserved. A role still referenced by a membership cannot be deleted either (409).

Member roles resolve into a member’s JWT perms claim at token exchange time.

The restricted catalog is the entire point. An end-user role may hold only:

  • data:<collection|*>:<read|list|create|update|delete>
  • storage:read, storage:write
  • functions:invoke

Never *. Never a bare data:* grant. Never the admin op. Never any :manage permission. An end-user role can therefore never grant an administrative capability, no matter how it is written.

{
"name": "premium",
"permissions": ["data:articles:read", "data:articles:list",
"data:comments:create", "storage:read"]
}

Assign it on the user:

Terminal window
curl -X PATCH "$URL/api/v1/admin/users/$USER_ID" \
-H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"metadata":{"role":"premium"}}'

The built-in user role is read-only. A role assigned to any user cannot be deleted (409).

Unknown role names resolve to user at token issuance — deliberately failing to the default rather than closed, so a user is never locked out of your app by a data problem. The admin write boundaries reject typos, so that fallback only ever covers legacy data.

An API key does not hold arbitrary permissions; it holds scopes that expand to them:

Scope Expands to
read data:*:read, data:*:list
write data:*:create, data:*:update, data:*:delete
admin *
storage storage:read, storage:write
functions functions:invoke

Transport gating on top: read for GETs and vector search, write for data mutations, admin for anything under /api/v1/admin/*. GraphQL derives it from the operation — queries need read, mutations need write.

A key is not a policy bypass. Its identity is uid = <key id>, so documents it creates are owned by the key; its role is Admin with the admin scope and service otherwise; and its context carries token: { apiKey: true, scopes }, which a policy rule can reference:

{ "delete": { "filter": {}, "validate": "auth.token.apiKey == true" } }

Managing keys:

Terminal window
curl -X POST "$URL/api/v1/admin/api-keys" \
-H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"name":"ci","scopes":["read","write"]}'

The raw key is returned exactly once. Only a SHA-256 hash is stored — a lost key cannot be recovered, only rotated. Rotation keeps the id, name and scopes and kills the old secret immediately. lastUsedAt is throttled to one write per key per minute, so it is approximate.

Caller Permissions
End user with a session Their end-user role’s list, from metadata.role.
Project member (after token exchange) Their member role’s list for that project.
API key The expansion of its scopes.
Anonymous None — only rules with no $auth.* placeholder can match.