The workspace authorisation model: roles are assigned via Clerk metadata, permissions
are strings, and a role unlocks a fixed set of permissions defined in lib/auth/rbac-shared.ts.
This page should be auto-generated from
lib/auth/rbac-shared.tsso it never drifts. See /operations/observability for the planned docs-gen script. The matrix below is hand-maintained until that lands.
| Role | Label | Hierarchy | Notes |
|---|---|---|---|
org:super_admin | Super Administrator | 1000 | Protected role. Has every permission. |
org:admin | Administrator | 900 | Everything except org:sys:manage. |
org:marketing_manager | Marketing Manager | 800 | Content + analytics + webinars + glossary CRUD. |
org:sales_manager | Sales Manager | 700 | Leads + analytics export. |
org:hr_manager | HR Manager | 700 | Jobs + candidates full management. |
org:legal_counsel | Legal Counsel | 700 | Policy editing + publishing + audit view. |
org:content_manager | Content Manager | 600 | Full content + glossary management. |
org:developer | Developer | 500 | API keys + webhooks + dev tools. |
org:publisher | Content Publisher | 400 | Content + glossary publish only. |
org:content_editor | Content Editor | 300 | Content + glossary edit (no delete/publish). |
org:sales_rep | Sales Representative | 200 | Leads view (no export). |
org:recruiter | Recruiter | 200 | Candidates view + manage. |
org:viewer | Viewer | 100 | No permissions — empty set ([]). A bare authenticated account with no role grants. |
Permissions follow the pattern org:<feature>:<action>. Categories:
org:sys:* — system administration (domains, SSO, audit logs).org:team:* — team management.org:billing:* — billing & invoices.org:content:* — content studio (create / edit / publish / delete / archive / view / SEO).org:leads:* — lead management (view / export / sync).org:analytics:* — analytics dashboards & exports.org:dev:* — developer tools (API keys, webhooks, internal tools).org:hrm:* — HR module (jobs, candidates).org:legal:* — legal policies & audit (policies_edit / policies_publish / audit_view).org:webinars:* — webinar CRUD, partners, registrations.org:glossary:* — glossary CRUD + analytics.org:observability:* — unified System Health module (11 permissions): view, services_view, integrations_view, integrations_run, diagnostics_view, logs_view, errors_view, webhooks_view, analytics_view, pii_view, manage.// API route handler
import { checkRole } from "@/lib/auth/rbac";
export async function POST(req: NextRequest) {
if (!(await checkRole("org:content:create"))) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// …mutation logic
}
checkRole reads the caller's roles from Clerk session claims (with an API fallback) and
returns true iff at least one of those roles grants the requested permission.
For Server Components, use the redirecting guard from lib/auth/guards.ts. On failure it calls
Next's redirect() (default target /workspace) rather than throwing:
import { requirePermission } from "@/lib/auth/guards";
export default async function AdminPage() {
await requirePermission("org:sys:manage"); // redirect("/workspace") on failure
return <Dashboard />;
}
// Override the redirect target with the optional second arg:
// await requirePermission("org:sys:manage", "/some/other/path");
Permission type in lib/auth/rbac-shared.ts.ROLE_PERMISSIONS.checkRole("org:new:permission") from the relevant route handler.Role union in lib/auth/rbac-shared.ts.ROLE_PERMISSIONS, ROLE_LABELS, ROLE_HIERARCHY.PROTECTED_ROLES.publicMetadata.role (the primary role string) plus publicMetadata.additionalRoles (an array of any extra role strings). There is no publicMetadata.roles field.