Subchapter 9.4
references/roles-permissions.mdMarkdown5 KBView on GitHub
Clerk Organizations use Role-Based Access Control (RBAC). Every member has one Role per org; Roles carry Permissions (a mix of Clerk-provided System Permissions and your own custom Permissions).
| Role | Default meaning |
|---|---|
org:admin | Full access — holds all System Permissions, can manage the Organization and its memberships |
org:member | Read-only members. By default has only org:sys_memberships:read and org:sys_billing:read |
Both slugs are automatically created when Organizations are enabled. You cannot delete a default Role if it’s set as the org’s Creator or Default Role — reassign to another Role first.
These are the only built-in Permissions. Use them verbatim; do NOT invent shorter forms like org:manage_members or org:create.
| Slug | Purpose |
|---|---|
org:sys_profile:manage | Edit Organization profile (name, slug, logo) |
org:sys_profile:delete | Delete the Organization |
org:sys_memberships:read | View the member list |
org:sys_memberships:manage | Invite, remove, and change roles of members |
org:sys_domains:read | View Verified Domains |
org:sys_domains:manage | Add / verify / remove Verified Domains |
org:sys_billing:read | View billing info (subscription, invoices) |
org:sys_billing:manage | Manage billing (change plan, payment method) |
Creator Role requirement. The Role that Clerk assigns to a user who creates a new Organization MUST carry at minimum:
org:sys_memberships:manageorg:sys_memberships:readorg:sys_profile:deleteIf you reassign the Creator Role, ensure the replacement Role has these three at minimum.
Up to 10 custom Roles per instance. Create via Dashboard → Roles & Permissions (opens in a new tab) → Add role, or via clerk api -X POST /v1/organization_roles with body {"name":"Billing Manager","key":"org:billing","description":"..."}. The key follows org:<role> format. Examples:
org:billing — carries org:sys_billing:manageorg:reports_viewer — carries your custom org:reports:viewNaming convention: org:<resource>:<action>. Examples: org:reports:view, org:api_keys:create, org:posts:edit.
Create via Dashboard → Roles & Permissions (opens in a new tab) → Permissions tab → Add permission, or via two steps: (1) clerk api -X POST /v1/organization_permissions with body {"name":"Edit Posts","key":"org:posts:edit","description":"..."} to create the permission, then (2) clerk api -X POST /v1/organization_roles/{role_id}/permissions/{permission_id} to attach it to a role. Permissions are attached to Roles inside a Role Set.
Roles are surfaced to Organizations through Role Sets — this controls which Roles can be assigned within a given Organization. Each Organization is assigned exactly one Role Set.
Default behavior: all orgs share the default Role Set. If you need per-org role variations (e.g. one customer org has its own org:support role), create a second Role Set and assign it.
When Clerk Billing is enabled, a custom Permission org:<feature>:<action> only returns true from has() if the <feature> part matches a Feature included in the organization’s active Plan.
Example: user has role org:admin with Permission org:posts:edit. has({ permission: 'org:posts:edit' }) returns:
false — if the active Plan does not include the posts Featuretrue — if the active Plan includes the posts FeatureThis applies regardless of role assignment. See clerk-billing skill for the full feature-vs-plan model.
Via Backend API:
import { clerkClient } from '@clerk/nextjs/server'
const clerk = await clerkClient()
await clerk.organizations.updateOrganizationMembership({
organizationId: 'org_123',
userId: 'user_123',
role: 'org:admin',
})Via Dashboard: Users → select member → change role dropdown.
Via UI: the <OrganizationProfile /> component’s Members tab includes a role-change dropdown for admins.
Three surfaces, same semantics:
// Server (Next.js)
import { auth } from '@clerk/nextjs/server'
const { has } = await auth()
has({ role: 'org:admin' })
has({ permission: 'org:sys_memberships:manage' })// Client (any React-based SDK)
import { useAuth } from '@clerk/nextjs'
const { has, isLoaded } = useAuth()
if (!isLoaded) return null
has?.({ role: 'org:admin' })// JSX conditional
import { Show } from '@clerk/nextjs'
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>org: prefix is mandatory for all org-scoped permissions.isLoaded before trusting has on the client. On first render has can be undefined — use optional chaining (has?.()) or guard on isLoaded.org:Admin is not org:admin.has() still returns the old value, the session token is stale. await clerk.session?.reload() on the client, or navigate to force a new session.