Permissions define what each role can do with a resource. Every permission ties
a role to an action (create, read, update, or delete) and specifies up
to three layers of access control: fields, filters, and checks.
A common pattern is letting users manage their own data while admins see
everything. Here is the complete permission set for a tasks resource where
each user can only access tasks they own:
[
{
"role": "user",
"action": "create",
"fields": ["title", "description", "status"],
"checks": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
},
{
"role": "user",
"action": "read",
"fields": ["id", "title", "description", "status", "created_at"],
"filters": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
},
{
"role": "user",
"action": "update",
"fields": ["title", "description", "status"],
"checks": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
},
{
"role": "user",
"action": "delete",
"checks": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
},
{
"role": "admin",
"action": "read",
"fields": ["id", "title", "description", "status", "owner_id", "created_at", "updated_at"]
},
{
"role": "admin",
"action": "delete"
}
]What this does:
- Create. Users supply
title,description, andstatus. Theowner_idis automatically injected from the authenticated user's ID (via theclaim), so users can never set it to someone else's ID. - Read. The filter on
owner_idensures users only see their own tasks. - Update / Delete. The check on
owner_idensures users can only modify or remove tasks they own. - Admin. No filters or checks, so admins see and manage all tasks.
The rest of this page explains each layer in detail.
The fields array lists the field names a role is allowed to interact with for
the given action. Only the specified fields are accepted in request bodies (for
writes) or returned in responses (for reads).
{
"role": "viewer",
"action": "read",
"fields": ["id", "title", "body", "created_at"]
}System fields (id, created_at, updated_at) are always accessible and do
not need to be explicitly listed.
The filters array applies WHERE-clause constraints that limit which records a
role can access. Filters are evaluated at query time and are primarily used for
read operations.
{
"role": "user",
"action": "read",
"filters": [
{ "field": "owner_id", "operator": "eq", "claim": "id" },
{ "field": "status", "operator": "neq", "value": "archived" }
]
}In this example, the user can only read records they own that are not archived.
The checks array applies validation constraints evaluated at write time. They
are used for create, update, and delete actions to enforce business rules
and enable automatic value injection.
{
"role": "user",
"action": "create",
"fields": ["title", "body"],
"checks": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
}When a check uses the eq operator with a claim or value, the target field
becomes optional in the request body and its value is automatically injected.
See Claims-Based Auto-Injection below.
Constraints used in filters and checks support 27 operators grouped by type:
| Operator | Description |
|---|---|
eq |
Equal |
neq |
Not equal |
gt |
Greater than |
lt |
Less than |
gte |
Greater than or equal to |
lte |
Less than or equal to |
| Operator | Description |
|---|---|
in |
Value in array |
nin |
Value not in array |
is_null |
Field is null |
is_not_null |
Field is not null |
| Operator | Description |
|---|---|
like |
SQL LIKE pattern matching |
ilike |
Case-insensitive LIKE |
similar |
SQL SIMILAR TO pattern |
regex |
Regular expression match |
iregex |
Case-insensitive regular expression |
| Operator | Description |
|---|---|
contains |
JSON contains key/value |
contained_in |
Value is contained in JSON |
has_key |
JSON object has specific key |
has_keys_any |
JSON has any of the given keys |
has_keys_all |
JSON has all of the given keys |
| Operator | Description |
|---|---|
exists |
Related record exists |
not_exists |
Related record does not exist |
| Operator | Description |
|---|---|
_and |
Logical AND |
_or |
Logical OR |
_not |
Logical NOT |
Each constraint object has the following structure:
{
"field": "owner_id",
"operator": "eq",
"value": "some-static-value",
"claim": "id"
}Use value for static comparisons and claim for dynamic values sourced from
the authenticated user.
When a check constraint uses the eq operator with either a claim or a
value, Snaapi automatically handles the target field:
- The field becomes optional in the request schema. Callers do not need to provide it.
- At request time, the value is injected automatically from the claim source or static value.
- If the caller provides a value for the field, it is overridden by the
injected value. This prevents tampering. Users cannot set their own
owner_id, for example.
Claims reference fields from the authenticated user object using dot notation:
| Claim Example | Resolves To |
|---|---|
id |
The user's ID |
email |
The user's email address |
metadata.team_id |
A nested value in user metadata |
Given this permission:
{
"role": "user",
"action": "create",
"fields": ["title", "body"],
"checks": [
{ "field": "owner_id", "operator": "eq", "claim": "id" }
]
}A user creating a post only needs to provide title and body. The owner_id
field is automatically set to the authenticated user's ID, and any user-supplied
owner_id value is discarded.
When an admin authenticates via an API key (admin token), resource endpoints restrict write operations:
| Operation | Allowed |
|---|---|
GET (read) |
yes |
DELETE |
yes |
POST (create) |
no |
PUT/PATCH (update) |
no |
Create and update requests return a 403 response with error code
ADMIN_TOKEN_NOT_ALLOWED. Use the dedicated admin endpoints for these
operations instead.
This restriction ensures that write operations by admin tokens go through dedicated admin endpoints with proper audit trails.
