Authio docs

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 denormalised permission_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:

  • memberis_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_roles rows.
  • 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 permissions claim 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.