Browse the docs
Concepts

RBAC & permissions

OrthID authorises every action with role-based access control. Roles bundle permissions, sessions carry scopes, and least privilege is the default.

Authentication answers “who is this?”. Authorisation answers “what may they do?”. OrthID answers the second with RBAC built from three primitives: permissions, roles and scopes. They apply uniformly to all three actors, so a human, an organisation and an agent are all checked the same way.

Permissions, roles and scopes

  • Permission - the smallest unit of access, written resource:action, for example records:read or members:invite.
  • Role - a named bundle of permissions, such as clinician or org:admin. You assign roles, not individual permissions.
  • Scope - the set of permissions resolved onto a live session or token. The scope is what your code actually checks at runtime.

Least privilege

OrthID grants nothing by default. A new actor has no permissions until a role is assigned, and an agent has no reach until a scope is issued. Always grant the narrowest role that lets the actor do its job, and prefer many small roles over one broad one. This keeps the blast radius of any single credential small.

Defining a role

Define roles as a bundle of permissions. Roles can be built in (org:admin, org:member) or custom to your product:

server: define a role
import { orthid } from "@orthid/sdk";

await orthid.roles.create({
  organization: "org_2bT7uX",
  key: "clinician",
  name: "Clinician",
  permissions: [
    "records:read",
    "records:write",
    "summaries:write",
  ],
});

Checking a permission

At runtime, verify the session and check the resolved scope. The scope already accounts for the actor’s roles in the active organisation, so you check one flat list:

app/api/records/route.ts
import { orthid } from "@orthid/sdk";

const { session } = await orthid.sessions.verify(token);

if (!session.scope.includes("records:write")) {
  return new Response("Forbidden", { status: 403 });
}

// proceed - the actor is allowed to write records

Org-scoped roles

Roles are scoped to an organisation, not global. The same human can be org:admin in one organisation and org:member in another. Because a session has one active organisation at a time, the resolved scope reflects only that organisation’s roles. Switching organisations (via <OrgSwitcher/>) re-resolves the scope.

How scopes apply to agents

Agents are RBAC citizens too, with one extra rule: an agent’s scope must be a subset of the human it acts on behalf of. When you issue an agent credential, OrthID intersects the scope you request with what the principal human is permitted in that organisation. The agent can never out-reach its human.

server: scope an agent
import { orthid } from "@orthid/sdk";

// Requested scope is intersected with user_3kP9aZ's permissions.
const agent = await orthid.agents.issue({
  onBehalfOf: "user_3kP9aZ",
  scope: ["records:read"],  // narrower than the human's full role
  ttl: "10m",
  region: "au-syd-1",
});
Requesting a permission the human lacks
If an agent’s requested scope includes a permission its principal human does not hold, the issue call is rejected. Scopes only ever shrink down the delegation chain.

Next steps