Concepts
Roles and permissions
Project-scoped roles + permissions, single-role default with a per-project multi-role toggle, JWT-shaped claims your app reads directly.
Authio’s roles surface mirrors the WorkOS shape one-for-one so a team migrating in can map their existing role catalogue without reshaping any JWT-reading code. Every role and every permission lives on a single Authio project; assignments hang off the memberships row.
The data model
Three tables and two project-level toggles:
roles—(project_id, slug)unique. Carries a denormalisedpermission_ids text[]so the JWT mint reads a single row to flatten the permissions claim.permissions—(project_id, slug)unique. Mirrors WorkOS’s flat slug namespace (organizations:manage,users:read, etc.).membership_roles— many-to-many join. Ships day-1; off-by-default in the JWT shape so the eventual single-to-multi-role flip is a per-project toggle and not a migration.projects.allow_multiple_roles boolean— flips single → multi role mode.projects.roles_action_override boolean— opt-in for Pattern 3 (Customer-as-source via Actions; see the integration patterns page).
System vs custom
Every new project is seeded with two system roles and twelve system permissions. They’re flagged is_system = true at the DB layer; the management API refuses to delete them or change their slug / is_system flag. You can rename them (display name + description) and you can change the admin role’s permission set.
The seeded system roles:
member—is_default = true. No system permissions. New memberships with no role specified get this role automatically.admin— holds every system permission. The “manage-everything” role for an org admin.
See the full system permission catalogue on the System permissions page.
Default role behaviour
Exactly one role per project may be marked is_default = true. A partial unique index enforces this at the schema level. New members joining an organization with no role specified are auto-assigned the default role.
To change the default, edit the new role’s Make this the default checkbox in the dashboard’s /roles/[id] page — saving will atomically flipis_default on the new role and clear it on the previous default. The management API rejects a second is_default = true insert with default_role_exists as a defense in depth.
Single-role vs multi-role mode
At GA, Authio runs in single-role mode: each membership holds exactly one role and the JWT carries “roles”: “admin” as a JSON string. This is what WorkOS and Clerk default to; it’s the predictable, audit-friendly shape that fits inside the 4 KB session-cookie cap with room to spare.
Flip projects.allow_multiple_roles = true in /settings/authorization to switch to multi-role mode:
- A membership may hold many roles via
membership_rolesrows. - The JWT shape changes to
“roles”: [“admin”, “billing-viewer”]— always a JSON array, even when only one role is assigned, so your SDK code parses it the same way every time. - The
permissionsclaim is always an array; in multi-role mode it’s the deduped union of every role’s permissions.
Flipping allow_multiple_roles from on to off is refused if any current membership holds >1 role — reduce the offending memberships to a single role first. This keeps the “single-role” promise sound on every token after the flip.
What lands on the JWT
Every sign-in (and every refresh / org-switch) re-resolves the active membership’s roles and emits two claims:
// single-role mode (default)
{
"sub": "user_01H…",
"act_org": "org_01H…",
"roles": "admin",
"permissions": [
"organizations:manage",
"organizations:read",
"users:manage",
"users:read"
]
}
// multi-role mode (allow_multiple_roles = true)
{
"sub": "user_01H…",
"act_org": "org_01H…",
"roles": ["admin", "billing-viewer"],
"permissions": [
"billing:read",
"organizations:manage",
"users:read"
]
}Both claim names — roles and permissions — are part of the reserved-claim set. You cannot override them via the T2.4 custom-claims surface; the only path to customer-controlled roles is the dedicated Pattern 3 Actions hook on /actions/pattern-3-customer-roles (see also the customer-roles source-of-truth concept page for the four supported integration patterns).
Where this lives in the dashboard
/roles— create / edit / delete roles, attach permissions./permissions— catalogue of grantable actions; create custom permissions and see which roles use each one./orgs/[id]?tab=members— per-member role dropdown reads from the project’s roles catalogue./settings/authorization— toggle multi-role mode + (eventually) Actions-based role override.
Customizing
Custom roles and custom permissions are first-class. Create a permission like invoices:approve in /permissions, then create a role like billing-approver in /roles with that permission selected, then assign the role to a member. The role shows up as “roles”: “billing-approver”on the next JWT issued for that membership.
For a deeper dive on the four ways customers wire their existing role catalogues into Authio, see the integration patterns guide.
