A role bundles permissions for one app. Users receive roles only through their groups — never directly.
User
↓ membership (transitive BFS)
Group(s)
↓ does BoundTo contain the requesting app? (otherwise: dormant)
active group(s)
↓ roles
Role(s) (with AppSlug)
↓ filter: Role.AppSlug == requesting app? (or permission is fully-qualified)
Permission(s) → app:resource:action
Effect: a user is Editor in Acme-Tasks because
- they are a member of a group
Acme-Tasks Team, - the group has
BoundTo: ["acme-tasks"], - the group references a role
Acme-Tasks EditorwithAppSlug = "acme-tasks", - the role's permissions
read,writeon resourcetodoexpand toacme-tasks:todo:read,acme-tasks:todo:write.
Modgud manages permissions as app:resource:action strings:
| Permission | Meaning |
|---|---|
modgud:user:read |
Read the user list in modgud |
modgud:oauth-client:write |
Edit OAuth clients in modgud |
acme-tasks:todo:write |
Write todos in the Acme-Tasks app |
Plus three bypass tiers:
realm:admin— realm-wide. The holder may do anything in any app.<app>:admin— app-wide.<app>:<resource>:admin— resource-wide.
When the first admin in a realm is created (recovery CLI or HTTP bootstrap-invite — see First-time setup), Modgud atomically seeds three roles — all under the system app modgud:
| Role | App | Effect |
|---|---|---|
| System Admin | modgud | holds the fully-qualified permission realm:admin → realm-wide bypass |
| User Manager | modgud | modgud:user:read/write + :session:read/write + :authorization-group:read + :permission-role:read + :auth-log:read |
| Viewer | modgud | read-only on user, authorization-group, permission-role |
Run node scripts/seed-demo.mjs after first login and you'll get additional roles for realistic test setups (see data/demo-seed.json for the manifest).
What resources an app has is defined by the app itself — see Applications. The system app modgud has these built in:
| Resource | Typical actions |
|---|---|
| app | read, write, admin (for app management itself) |
| user | read, write |
| session | read, write |
| permission-role | read, write |
| authorization-group | read, write |
| oauth-client | read, write |
| oauth-scope | read, write |
| oauth-api | read, write |
| login-provider | admin, read, write |
| idp-config | read, write |
| realm | read, write |
| auth-log | read |
| gdpr | admin |
External apps (Acme-Tasks, Knowledge, …) bring their own resources, defined in their App record.
Administration → Roles → Create, or double-click an entry.
Fields:
- Name (unique per realm)
- Description (optional)
- AppSlug — which app does this role belong to? Required. A role belongs to exactly one app.
- Resource Type — together with AppSlug determines the permission prefix
- Permissions — actions on the resource. With Resource Type
todoand Permissions["read", "write"], the role resolves to<AppSlug>:todo:readand<AppSlug>:todo:write.
If you want a role to span several resources (e.g. "User Manager" covers user, session, authorization-group), leave Resource Type empty and write fully-qualified permissions in the list:
modgud:user:read
modgud:user:write
modgud:session:read
modgud:authorization-group:read
Fully-qualified strings (containing :) pass through the resolver unchanged. The seeded System Admin / User Manager / Viewer roles are built exactly this way.
To make a variant of a role — say a tighter copy of an existing one — right-click it in the list → Clone. The Create modal opens pre-filled: the linked Application, the selected permission subset and the realm-admin flag are copied; only the Name is blank. Give the copy a new name, adjust the permission selection, and create.
A role can also include fully-qualified permissions from other apps in its permissions list — for example a "Cross-App Auditor" with modgud:auth-log:read AND acme-tasks:audit:read. This works because fully-qualified permissions pass through without further filtering.
In practice though: prefer two separate roles in two separate groups (each with their own BoundTo). Cleaner to understand and audit.
A role becomes a bypass role when its permissions list contains an admin-shaped entry:
| In the permissions list | Effect |
|---|---|
realm:admin (fully qualified) |
realm-wide bypass |
<app>:admin |
app-wide bypass |
<app>:<resource>:admin (Resource Type empty + fully qualified) |
resource-wide |
admin (with Resource Type set) |
resource-wide, AppSlug-prefixed |
On setup exactly one user is seeded as realm admin (System Admin role + Administratoren group with BoundTo: ["*"]). Grant sparingly — realm-admin is the nuclear option.
List → right-click → Delete.
::: warning Soft delete Roles are soft-deleted. Groups that referenced the role keep the entry technically — but the role contributes no permissions any more. To remove a role cleanly, remove it from all groups first. :::
::: tip Keep roles narrow
Many small roles, each tied to a clear resource, compose freely into groups. A "SuperAdmin" role with every permission is usually a design smell; use realm:admin for that, or combine specialised roles in an admin group.
:::
::: tip Per-app roles
Roles for Acme-Tasks go under AppSlug = "acme-tasks", not modgud. They show up in the right permission lists, and [Authorize(Roles = "...")] in the Acme-Tasks backend finds them via the resource_access["acme-tasks"] claim in the token.
:::

