Permissions
Three-layer access control over roles, actions, and fields.
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 role with no permissions on a resource has no access to that resource. The
default for any new role is "no access". API keys are assigned a role the same
way users are, so a key scoped to viewer will see the same fields as a
viewer user. This keeps the permission model consistent across human and
machine traffic.
Quick example: multi-tenant task list
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": "=", "value": "$user.id" }
]
},
{
"role": "user",
"action": "read",
"fields": ["id", "title", "description", "status", "created_at"],
"filters": [
{ "field": "owner_id", "operator": "=", "value": "$user.id" }
]
},
{
"role": "user",
"action": "update",
"fields": ["title", "description", "status"],
"checks": [
{ "field": "owner_id", "operator": "=", "value": "$user.id" }
]
},
{
"role": "user",
"action": "delete",
"checks": [
{ "field": "owner_id", "operator": "=", "value": "$user.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 the$user.idreference), 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 three-layer model
Fields
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.
Filters
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": "=", "value": "$user.id" },
{ "field": "status", "operator": "!=", "value": "archived" }
]
}
In this example, the user can only read records they own that are not archived.
Checks
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": "=", "value": "$user.id" }
]
}
When a check uses the = operator with a $user.<attribute> reference, the
target field becomes optional in the request body and its value is automatically
injected. See Claim-based auto-injection below.
Constraint operators
Constraints used in filters and checks support 14 operators across 4 groups.
Comparison
| Operator | Description |
|---|---|
= |
Equal |
!= |
Not equal |
< |
Less than |
<= |
Less than or equal to |
> |
Greater than |
>= |
Greater than or equal to |
Null
| Operator | Description |
|---|---|
is_null |
Field is null |
is_not_null |
Field is not null |
String
| Operator | Description |
|---|---|
contains |
Substring match |
starts_with |
Value begins with the given input |
ends_with |
Value ends with the given input |
regex |
Regular expression match |
Array
| Operator | Description |
|---|---|
in |
Value is in the given list |
not_in |
Value is not in the list |
Each constraint object has the following structure.
{
"field": "owner_id",
"operator": "=",
"value": "some-static-value"
}
Use a plain string for value for static comparisons, or a $user.<attribute>
reference for dynamic values sourced from the authenticated user (see below).
Null operators (is_null, is_not_null) omit value entirely.
Claim-based auto-injection
When a check constraint uses the = operator with a value that references the
authenticated user (such as $user.id), 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 resolved user attribute.
- 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.
User attribute references
The $user. prefix resolves to attributes on the authenticated user. The
runtime supports four attributes.
| Reference | Resolves To |
|---|---|
$user.id |
The user's ID |
$user.email |
The user's email address |
$user.name |
The user's display name |
$user.role |
The user's role identifier |
Example
Given this permission:
{
"role": "user",
"action": "create",
"fields": ["title", "body"],
"checks": [
{ "field": "owner_id", "operator": "=", "value": "$user.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.
Admin token restrictions
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.
Auditing
Every action evaluated by the permission engine produces an entry in the audit log. The entry records who attempted the action, what was attempted, whether it was allowed, and the field set involved.