RBAC — Roles and permissions
Auth Service implements per-application RBAC. Each application has its own isolated set of roles and permissions. A user can hold different roles in different applications.
Model
Application
└── Role (e.g. "admin", "editor", "viewer")
└── Permission (resource + action, e.g. "documents.write")
User ── UserAppRole ──► Role (scoped to Application)
Roles
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
applicationId | UUID | Application this role belongs to |
name | string | Role name (unique per application, max 64 chars) |
description | string | Optional description (max 250 chars) |
isDefault | boolean | Assigned to a user who is granted access (or auto-provisioned on a public / open-registration application) without an explicit role |
Only one role per application can have isDefault = true. Creating a new default role resets the existing default.
Permissions
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
applicationId | UUID | Application this permission belongs to |
resource | string | Resource name, e.g. documents, reports (max 128 chars, must match /^[a-zA-Z][a-zA-Z0-9._-]*$/) |
action | "read" | "write" | Action on the resource (default read) |
Permission strings in the JWT follow this convention:
readaction →resource(e.g.documents)writeaction →resource.write(e.g.documents.write)
When a user holds both the read and the write permission on the same resource, only resource.write is emitted.
Admin API endpoints
All role and permission endpoints require an admin or superadmin session.
List roles with permissions
GET /api/admin/applications/:appId/roles
Returns all roles for the application, each with a permissionIds array.
Create a role
POST /api/admin/applications/:appId/roles
{
"name": "editor",
"description": "Can read and write documents",
"isDefault": false
}
A duplicate name is refused with PERM_004.
Update a role
PATCH /api/admin/applications/:appId/roles/:roleId
{ "description": "Can edit content", "isDefault": false }
Only description and isDefault can be changed; the name is immutable (a payload containing name is rejected with 400 APP_001).
Delete a role
DELETE /api/admin/applications/:appId/roles/:roleId
Blocked (PERM_006) while the role is assigned to any user of the application.
Assign a permission to a role
POST /api/admin/applications/:appId/roles/:roleId/permissions
{
"permissionId": "uuid-1"
}
One permission per call; assigning it again is a no-op. Both the role and the permission must belong to the application (PERM_002 / PERM_003 otherwise).
List permissions
GET /api/admin/applications/:appId/permissions
Create a permission
POST /api/admin/applications/:appId/permissions
{
"resource": "documents",
"action": "write"
}
Permission names must match /^[a-zA-Z][a-zA-Z0-9._-]*$/ (PERM_001 otherwise); a duplicate resource + action pair is refused with PERM_005.
Delete a permission
DELETE /api/admin/applications/:appId/permissions/:permissionId
JWT claims
When a client requests the roles and/or permissions scopes, the ID token, the userinfo response and a JWT access token include:
{
"roles": ["editor"],
"permissions": ["documents.write", "reports"]
}
Claims are populated by getUserClaims() in src/services/claims.ts. If the user has no role in the application, both arrays are empty.
Global user roles
In addition to per-application roles, every user has a global system role managed by BetterAuth's admin plugin. Roles are ranked user < admin < superadmin:
| Role | Capabilities |
|---|---|
user | Default. No admin API access. |
admin | Can manage users, sessions, and applications via the admin API, but only users of a lower rank (non-admin users). Can impersonate non-admin users. |
superadmin | Full admin privileges including impersonating admin users. Cannot be deleted if it is the last superadmin. |
Global roles are set via PATCH /api/admin/users/:id (or role on POST /api/admin/users) and are stored in BetterAuth's user table. Only user and admin can be assigned through the API, and only by a superadmin for admin; the first superadmin is created at startup from ADMIN_EMAIL / ADMIN_PASSWORD when no superadmin exists yet.