Authorization
Authentication answers “who are you?” Authorization answers “what are you allowed to do?” Auris handles both, but authorization is the deeper concern: almost every API call, every UI route, and every data read goes through at least one authorization check before it completes. This page explains how the system is structured, how each layer works, and how to apply the right layer for your use case.
Three Layers
Auris authorization is built in three distinct layers that stack on top of each other. Each layer adds precision.
| Layer | Mechanism | Granularity | Typical question answered |
|---|---|---|---|
| 1 — Authentication | Keycloak JWT | Per-tenant user identity | ”Is this user authenticated and do they belong to this tenant?“ |
| 2 — RBAC | Permission strings on roles | Per-action-type | ”Does this user have the general right to perform this kind of action?“ |
| 3 — FGA | Zanzibar relationship tuples | Per-resource instance | ”Does this user have access to this specific resource?” |
In most API routes you will use Layer 2 alone. When your application manages resources that different users own or share (documents, projects, calendars, tickets), you add Layer 3 on top.
Layer 1 — Authentication (Keycloak JWT)
Every request starts with a signed JWT issued by Keycloak. The token is validated on every request by protectRoute() / requireAuth() in lib/auth/protect.ts. Validation includes:
- Signature verification using Keycloak’s public keys
- Expiry and not-before checks
- Token blacklist check (for explicitly invalidated sessions)
The tenant is resolved from the x-tenant request header or from the iss claim in the JWT (which contains /realms/{realm} — the realm name maps directly to an Auris Tenant record).
If the token is missing, malformed, blacklisted, or does not belong to the requested tenant, the request fails with 401 Unauthorized before any permission logic runs.
Layer 2 — RBAC
Once a request is authenticated, the permission engine checks whether the user’s roles grant the required permission. Permissions are action:resource strings (e.g., manage:users, view:invoices). They are stored in Prisma across three tables:
PermissionDefinition— the catalog of all known permission stringsRolePermission— which permissions are assigned to a role, with an effect ofALLOWorDENYUserPermissionOverride— per-user permission grants or denials that bypass role evaluation, with an optionalexpiresAtfor time-boxed grants
Resolution order (Discord-style priority):
- User DENY override — if the user has an explicit DENY for this permission, deny immediately regardless of roles.
- User ALLOW override — if the user has an explicit ALLOW override (and no DENY), grant immediately.
admin:allrole bypass — if any of the user’s roles carriesadmin:allas an ALLOW, every permission is granted. (A user DENY override foradmin:allitself can block this bypass.)- Role evaluation — if ANY role has a DENY for this permission, deny. If ANY role has an ALLOW (and none DENY), grant.
- Default deny — if nothing matched, deny.
This means denies always win within a tier, and user-level overrides always win over role-level grants.
For a deeper treatment of role creation, permission assignment, and the API endpoints that manage them, see the RBAC guide.
Layer 3 — Fine-Grained Authorization (FGA)
RBAC tells you whether a user has the right to perform an action on a resource type. FGA tells you whether a user has access to a specific resource instance. The distinction matters whenever different users legitimately own or share different objects of the same type.
FGA in Auris is a Zanzibar-compatible engine backed by relationship tuples stored in the RelationshipTuple Prisma table. A tuple looks like:
documents:doc_abc123 viewer user:usr_xyz456That tuple says: user usr_xyz456 is a viewer of document doc_abc123. Authorization checks traverse the graph of tuples to determine whether a relationship exists.
For the full model syntax, check algorithm, rewrite types, and modeling patterns, see Fine-Grained Authorization (Zanzibar Model).
Permission Strings
Every RBAC permission is an action:resource string. The action describes the operation; the resource describes the domain entity it targets.
manage:users view:audit issue:invoices
create:documents delete:expenses debug:fga
ai:use console:access admin:allBuilt-in permission categories
| Category | Example strings |
|---|---|
| Super admin | admin:all |
| Roles | manage:roles, view:roles, assign:roles |
| Users | manage:users, view:users |
| Permissions | manage:permissions, view:permissions |
| Audit | view:audit, export:audit, view:audit_logs |
| Tenant | manage:tenant, view:tenant |
| Security | manage:security, view:security |
| System | manage:system, view:system |
| Content | manage:content, collaborate:content, share:content |
| User data | manage:user_data, export:user_data |
| Integrations | manage:integrations, view:integrations |
| Marketplace | view:marketplace, manage:marketplace, install:integrations, developer:marketplace |
| Sessions | manage:sessions |
| SSO | manage:sso_connections, view:sso_connections |
| SCIM | manage:scim_connections, view:scim_connections, view:scim_logs |
| Identity providers | manage:identity_providers, view:identity_providers |
| Members and teams | manage:members, view:members |
| Console | console:access |
| FGA | view:fga_models, manage:fga_models, view:fga_tuples, manage:fga_tuples, debug:fga |
| Licensing | manage:license-policies, manage:license-keys, view:license-stats |
| AI | ai:use |
| Documents (invoices, quotes, orders, expenses, interventions…) | view:invoices, create:invoices, edit:invoices, delete:invoices, export:invoices, issue:invoices, manage:invoice_payments, approve:expenses, sign:interventions |
| Payments and billing | view:billing, manage:billing, manage:subscriptions, pay:invoices, refund:payments, view:disputes, manage:dunning |
The special permission admin:all grants every other permission when held on a role. It can still be blocked by a user-level DENY override, making it possible to lock a specific user out of super-admin access without removing the role itself.
Roles
A role is a named collection of permission grants and denials, scoped to a tenant. Users can hold multiple roles simultaneously. When a user holds multiple roles the engine merges them: if any role grants a permission and no role denies it, the permission is granted.
Roles also carry a position field (a numeric rank). Lower numbers indicate higher privilege. The role hierarchy controls which users can manage which other users: a user cannot act on another user whose highest-priority role outranks their own. getUserHighestPriority() returns the lowest position number across all of a user’s roles; 0 means most privileged, 999 means no roles assigned.
Create and manage roles via the Auris Console (Roles section) or via the API:
POST /api/roles Create a role
PATCH /api/roles/{id} Update name / description
POST /api/roles/{id}/permissions Assign permissions
DELETE /api/roles/{id}/permissions Remove permissions
POST /api/users/{id}/roles Assign a role to a user
DELETE /api/users/{id}/roles/{rid} Remove a role from a userFor detailed API reference and examples see the RBAC guide.
Fine-Grained Authorization
RBAC is appropriate for gates that apply uniformly across all instances of a resource type — “can this user create documents at all?” FGA is necessary when the answer depends on which specific instance is being accessed — “can this user edit this particular document?”
Standard relations and namespaces
The Auris platform defines four standard relations that apply across all resource types:
| Relation | Meaning |
|---|---|
owner | Full control — read, write, share, delete |
editor | Read and write |
viewer | Read only |
shared | Custom, application-defined access |
The standard namespaces correspond to core platform resource types:
documents domains vps calendars channels ticketsYour application can define additional namespaces by creating an authorization model in the console.
The two-call pattern
The canonical pattern for protecting a resource instance combines both layers:
// 1. RBAC gate — does this user have the general right to edit documents?
const result = await requirePermission(req, 'edit:documents')
if (!result.success) return result.response
// 2. FGA gate — does this user have access to THIS document?
const access = await requireResourceAccess(req, result.user.id, 'editor', 'documents', documentId)
if (!access.success) return access.response
// Proceed with business logicrequireResourceAccess() must always be called after requirePermission(), not instead of it. RBAC provides the coarse-grained type-level gate; FGA provides the fine-grained instance-level gate. Skipping RBAC and relying on FGA alone means a user with no document permissions at all could still be granted individual-document access through a tuple, which breaks the intended separation.
The bridge functions in lib/permissions/resource-access.ts manage tuple writes and reads:
| Function | Purpose |
|---|---|
checkResourceAccess(userId, relation, namespace, resourceId) | Returns a boolean — programmatic check |
checkAnyResourceAccess(userId, relations[], namespace, resourceId) | True if any of the listed relations holds |
grantResourceAccess(userId, relation, namespace, resourceId) | Writes a tuple (grants access) |
revokeResourceAccess(userId, relation, namespace, resourceId) | Deletes a tuple (revokes access) |
listResourceAccessors(namespace, resourceId, relation?) | Lists all subjects with access to a resource |
listUserResources(userId, namespace, relation?) | Lists all resources accessible to a user |
transferResourceOwnership(fromUserId, toUserId, namespace, resourceId) | Atomically transfers the owner tuple |
requireResourceAccess(req, userId, relation, namespace, resourceId) | Middleware — throws 403 if the check fails |
For the FGA model DSL, check algorithm, union/intersection/exclusion rewrites, and the console debugger, see Fine-Grained Authorization (Zanzibar Model).
The FGA engine is toggled by the FGA_ENGINE_ENABLED environment variable. When true, all checks run through the internal Zanzibar engine backed by the RelationshipTuple Prisma table. When false, the system falls back to Ory Keto via lib/permissions/keto-client.ts. New deployments should enable the internal engine; the Keto fallback exists as a migration path for tenants moving from an older Auris deployment.
Server-Side Enforcement
requirePermission()
requirePermission(req, permission) is the primary RBAC guard for API routes. It is defined in lib/permissions/permission-checker.ts and performs the full authentication + permission resolution pipeline in a single call:
- Extract the Bearer token from the
Authorizationheader. - Decode the JWT to get the Keycloak user ID (
sub/user_id). - Resolve the tenant name from the
x-tenantheader or theissclaim. - Look up the
Tenantrecord in Prisma —404if the tenant does not exist. - Look up the
TenantUserrecord —404if the user is not a member of this tenant. - Fetch the user’s Keycloak roles via
roleService.getUserRoles(). - Fetch role memberships from the authorization backend via Keto / FGA.
- Merge role names and permission strings into a unified permission set.
- Check whether the required permission is present —
403if not. - Return
{ success: true, user, roles, permissions }.
The return value is a discriminated union:
// Success — proceed
const result = await requirePermission(req, 'manage:users')
if (!result.success) return result.response // 401 or 403
const { user } = result // TenantUser recordrequireAnyPermission(req, permissions[]) accepts an array — the first matching permission grants access.
Via the SDK (recommended for app developers)
If you are building an application on top of Auris rather than working inside the Auris API itself, use the @auris/nextjs SDK wrapper:
import { requirePermission } from '@auris/nextjs/server'
// In a Next.js route handler or Server Action
await requirePermission('view:invoices')The SDK handles token extraction, tenant resolution, and error responses. See the RBAC guide for the full SDK reference.
Client-Side Checks
Client-side permission checks are display guards only. They control what the UI renders — they do not enforce security. Always enforce permissions on the server.
The @auris/react SDK provides hooks for reading the current user’s permissions inside React components.
import { useCheckPermission, usePermissions, PermissionGate } from '@auris/react'
// Check a single permission
const canManageUsers = useCheckPermission('manage:users')
// Bulk — get all permissions
const { permissions, loading } = usePermissions()
// Declarative gate component
<PermissionGate permission="manage:users">
<UserManagementPanel />
</PermissionGate>useCheckPermission() returns true | false | null (null during the initial load). PermissionGate renders its children only when the permission is granted; it renders nothing (or a fallback via the fallback prop) otherwise.
RBAC vs FGA — Decision Guide
| Question | Use RBAC | Use FGA |
|---|---|---|
| Does the check apply to all instances of a resource type? | yes | no |
| Does the check depend on who owns a specific object? | no | yes |
| Does the check depend on sharing or delegation? | no | yes |
| Is the access pattern the same for every user in a role? | yes | no |
| Do you need to list all objects a user can access? | no | yes |
| Is this a global capability like “can create any invoice”? | yes | no |
A common misconception is that FGA replaces RBAC. It does not. RBAC gates capabilities at the type level (“this user class can perform this kind of action”). FGA gates access at the instance level (“this user can access this specific object”). The two-call pattern described above is the canonical way to use both together.
Use RBAC alone when:
- Access is uniform across all resource instances (e.g., “only admins can access the security settings”)
- You are protecting a system-level operation with no per-instance ownership concept
Add FGA when:
- Different users own different instances of the same resource type
- Resources can be shared with specific users or groups
- You need to delegate access without changing roles
Related Pages
- RBAC guide — permission string reference, role management, SDK API
- Fine-Grained Authorization (Zanzibar Model) — tuple model, DSL, check algorithm, debugger
- Multi-Tenancy — how tenants scope all roles, permissions, and FGA models
- Sessions — token lifecycle and how authentication feeds into authorization