Authorization
Hierarchical role-based access control — roles with levels, scope:action permissions, and the union model that decides what a user can do.
The Auth API uses hierarchical role-based access control (RBAC) with scope:action permissions. Roles carry a numeric level from 1 to 100 that controls who can manage whom. A user's effective permission set is the union of their role-derived permissions and any direct grants.
Vocabulary
| Term | Definition |
|---|---|
| Permission | A scope:action pair like users:create, roles:assign, tenants:read. |
| Role | A named group of permissions, scoped to a tenant, with a level 1–100. |
| Level | Integer 1–100. Higher levels manage lower levels. System roles use 10/50/90/100. |
| Direct grant | A permission attached to a single user, optionally with an expiresAt. |
Permission format
Every permission is scope:action:
users:create
users:read
users:update
users:delete
roles:assign
roles:revoke
permissions:grant
permissions:revoke
tenants:update
client-keys:create
auth:logsCustom permissions follow the same pattern. The Auth API ships with 24
system permissions seeded into every new tenant. Custom permissions live
alongside them in the same table; they are distinguished by an is_system
flag.
System roles
When a tenant is created, four roles are seeded:
| Role | Level | Notes |
|---|---|---|
super_admin | 100 | Full access. Cannot be deleted. |
admin | 90 | Tenant administrator. Cannot manage super_admin. |
manager | 50 | Can manage user-level accounts only. |
user | 10 | Default end-user role. |
System roles cannot be deleted or have their level changed. You can, however, grant additional permissions to a system role on a per-tenant basis.
The hierarchy rule
A user cannot manage another user, role, or grant whose level is greater than or equal to their own highest role level.
A manager (level 50) can:
- Assign roles up to level 49.
- Promote/demote a
user(level 10). - Revoke permissions from a
user.
A manager cannot:
- Touch another
manager,admin, orsuper_admin. - Create a custom role at level ≥ 50.
- Grant permissions that aren't already in their own permission set.
Violations return HIERARCHY_VIOLATION (HTTP 403) with both the actor and
target levels in the error payload — handy for client-side messages.
Effective permissions
A user's effective permission set is:
effectivePermissions = union(rolePermissions, directGrants)Roles can be assigned to users with an optional expiresAt. Direct grants
also support expiresAt. Expired entries are filtered out at read time and
never embedded in newly issued tokens.
You can inspect the breakdown at any time:
GET /api/v1/permissions/user/{userId}{
"success": true,
"data": {
"userId": "01HXY...",
"rolePermissions": ["users:read", "users:update"],
"individualPermissions": ["client-keys:create"],
"effectivePermissions": ["users:read", "users:update", "client-keys:create"]
}
}Authorization in code
Endpoints are protected by middlewares applied per route:
| Middleware | Behavior |
|---|---|
requirePermission('users:create') | Single permission check. |
requireAnyPermission(['a:b','c:d']) | OR — passes if user has any. |
requireAllPermissions(['a:b','c:d']) | AND — passes only if user has all. |
requirePermissionDualAuth(...) | Same as above, but accepts JWT or Client Key. |
These checks read from the JWT payload, not the database — so the most
recent permission edit only takes effect after the user's next access-token
refresh (max ~15 minutes later by default). For checks that must be real-time,
call POST /api/v1/permissions/check and compare against the live database.
How to grant or revoke
POST /api/v1/permissions/grant{
"userId": "01HXY...",
"permissionId": "01HZ1...",
"expiresAt": "2026-12-31T23:59:59Z"
}POST /api/v1/permissions/revoke{
"userId": "01HXY...",
"permissionId": "01HZ1..."
}POST /api/v1/roles/assign{
"userId": "01HXY...",
"roleId": "01HZ2...",
"expiresAt": null
}POST /api/v1/roles/remove{
"userId": "01HXY...",
"roleId": "01HZ2..."
}Both grants and revocations write to the audit log.