> clerk-enterprise-rbac
Configure enterprise SSO, role-based access control, and organization management. Use when implementing SSO integration, configuring role-based permissions, or setting up organization-level controls. Trigger with phrases like "clerk SSO", "clerk RBAC", "clerk enterprise", "clerk roles", "clerk permissions", "clerk organizations".
curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/clerk-enterprise-rbac?format=md"Clerk Enterprise RBAC
Overview
Implement enterprise-grade role-based access control, organization management, and SSO with Clerk. Covers custom roles and permissions, organization lifecycle, multi-tenant access patterns, SAML/OIDC SSO, and the Backend API for programmatic role management (released Nov 2025).
Prerequisites
- Clerk Pro or Enterprise plan (Organizations + SSO require paid plan)
- Organizations feature enabled in Clerk Dashboard > Organizations > Settings
- Next.js 14+ with App Router (examples use
@clerk/nextjs)
Instructions
Step 1: Enable Organizations and Add UI Components
// app/org-selector/page.tsx
import { OrganizationSwitcher, OrganizationProfile } from '@clerk/nextjs'
export default function OrgPage() {
return (
<div className="p-8">
<h1>Select Organization</h1>
<OrganizationSwitcher
hidePersonal={false}
afterSelectOrganizationUrl="/dashboard"
afterCreateOrganizationUrl="/dashboard"
/>
<div className="mt-8">
<OrganizationProfile />
</div>
</div>
)
}
Step 2: Define Custom Roles and Permissions
Configure in Clerk Dashboard > Organizations > Roles and Permissions.
Default roles (built-in):
| Role | Key | Built-in Permissions |
|---|---|---|
| Admin | org:admin | Full org management (members, settings, billing) |
| Member | org:member | View org, read-only access |
Custom permissions (create in Dashboard > Organizations > Permissions):
| Permission | Key | Description |
|---|---|---|
| Read data | org:data:read | View organization resources |
| Write data | org:data:write | Create/update resources |
| Delete data | org:data:delete | Delete resources |
| Manage billing | org:billing:manage | Access billing settings |
| View analytics | org:analytics:read | Access analytics dashboard |
Custom roles (create in Dashboard > Organizations > Roles):
| Role | Permissions | Use Case |
|---|---|---|
org:manager | data:read, data:write, analytics:read | Content managers |
org:viewer | data:read | Read-only stakeholders |
org:billing_admin | data:read, billing:manage | Finance team |
Step 3: RBAC Middleware — Route Protection by Role
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)',
])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isManagerRoute = createRouteMatcher(['/manage(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isPublicRoute(req)) return
if (isAdminRoute(req)) {
// Only org:admin can access /admin/*
await auth.protect({ role: 'org:admin' })
} else if (isManagerRoute(req)) {
// org:admin OR org:manager can access /manage/*
await auth.protect((has) =>
has({ role: 'org:admin' }) || has({ role: 'org:manager' })
)
} else {
// All other routes just require authentication
await auth.protect()
}
})
Step 4: Permission Checks in Server Components
// app/admin/page.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function AdminPage() {
const { userId, orgId, orgRole, has } = await auth()
if (!userId) redirect('/sign-in')
if (!orgId) redirect('/org-selector')
// Permission-based checks (preferred over role-based)
const canManageMembers = has({ permission: 'org:sys_memberships:manage' })
const canWriteData = has({ permission: 'org:data:write' })
const canDeleteData = has({ permission: 'org:data:delete' })
const canViewAnalytics = has({ permission: 'org:analytics:read' })
return (
<div>
<h1>Admin Panel</h1>
<p>Current role: {orgRole}</p>
<nav>
{canManageMembers && <a href="/admin/members">Manage Members</a>}
{canWriteData && <a href="/admin/content">Content Management</a>}
{canDeleteData && <a href="/admin/danger-zone">Danger Zone</a>}
{canViewAnalytics && <a href="/admin/analytics">Analytics</a>}
</nav>
</div>
)
}
Step 5: Permission Checks in Client Components
'use client'
import { Protect, useOrganization, useAuth } from '@clerk/nextjs'
export function AdminSection() {
const { organization } = useOrganization()
const { has } = useAuth()
return (
<div>
<h2>{organization?.name}</h2>
{/* Declarative: Protect component with fallback */}
<Protect
role="org:admin"
fallback={<p>You need admin access to view this section.</p>}
>
<DangerZone />
</Protect>
{/* Permission-based rendering */}
<Protect permission="org:data:write">
<EditForm />
</Protect>
{/* Imperative: has() for conditional logic */}
{has?.({ permission: 'org:analytics:read' }) && (
<AnalyticsDashboard />
)}
</div>
)
}
Step 6: Organization Member Management via Backend API
// app/api/org/members/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
export async function GET() {
const { orgId, has } = await auth()
if (!orgId) return Response.json({ error: 'No org selected' }, { status: 400 })
if (!has({ permission: 'org:sys_memberships:read' })) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
const client = await clerkClient()
const members = await client.organizations.getOrganizationMembershipList({
organizationId: orgId,
})
return Response.json({
members: members.data.map(m => ({
userId: m.publicUserData?.userId,
name: `${m.publicUserData?.firstName} ${m.publicUserData?.lastName}`,
email: m.publicUserData?.identifier,
role: m.role,
joinedAt: m.createdAt,
})),
})
}
export async function POST(req: Request) {
const { orgId, userId, has } = await auth()
if (!orgId || !has({ permission: 'org:sys_memberships:manage' })) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
const { emailAddress, role } = await req.json()
const client = await clerkClient()
const invitation = await client.organizations.createOrganizationInvitation({
organizationId: orgId,
emailAddress,
role: role || 'org:member',
inviterUserId: userId!,
})
return Response.json({ invitation: { id: invitation.id, emailAddress, role } })
}
Step 7: Programmatic Role/Permission Management (Backend API)
// lib/org-roles.ts — manage roles and permissions via API (released Nov 2025)
import { clerkClient } from '@clerk/nextjs/server'
export async function createCustomRole(orgId: string) {
const client = await clerkClient()
// Create a custom permission
await client.organizations.createOrganizationPermission({
organizationId: orgId,
name: 'Manage reports',
key: 'org:reports:manage',
description: 'Create, edit, and delete reports',
})
// Create a custom role with that permission
await client.organizations.createOrganizationRole({
organizationId: orgId,
name: 'Report Manager',
key: 'org:report_manager',
description: 'Can manage all reports',
permissions: ['org:reports:manage', 'org:data:read'],
})
}
// Update a member's role
export async function updateMemberRole(
orgId: string,
userId: string,
newRole: string
) {
const client = await clerkClient()
const memberships = await client.organizations.getOrganizationMembershipList({
organizationId: orgId,
})
const membership = memberships.data.find(
m => m.publicUserData?.userId === userId
)
if (!membership) throw new Error('User is not a member of this organization')
await client.organizations.updateOrganizationMembership({
organizationId: orgId,
userId,
role: newRole,
})
}
Step 8: SAML SSO Configuration
Configure in Clerk Dashboard > SSO Connections > Add SAML Connection:
- ACS URL:
https://<your-clerk-frontend-api>.clerk.accounts.dev/v1/saml/acs - Entity ID:
https://<your-clerk-frontend-api>.clerk.accounts.dev/v1/saml/metadata - Upload IdP metadata XML from your provider (Okta, Azure AD, Google Workspace)
- Map SAML attributes:
email,firstName,lastName
// Enforce SSO for specific email domains
// Clerk Dashboard > Organizations > Settings > "Verified domains"
// Add your company domain (e.g., acme.com)
// Users with @acme.com emails will be forced through SSO
Error Handling
| Error | Cause | Solution |
|---|---|---|
orgId is null | No active organization | Redirect to org selector, show <OrganizationSwitcher /> |
has() returns false | Role/permission not assigned | Check assignment in Dashboard > Organizations > Members |
| Permission denied on middleware | User lacks required role | Verify route matcher maps to correct role |
| SSO login fails | Misconfigured IdP metadata | Verify ACS URL and Entity ID in IdP settings |
| Invitation fails | Email already a member | Check membership before inviting |
| Custom role not visible | Created via API, not Dashboard | Roles created via API are org-scoped, not instance-wide |
Enterprise Considerations
- Roles and permissions are embedded in the session JWT -- no extra network requests needed for authorization checks
- Custom roles created in the Dashboard are instance-wide; roles created via Backend API are organization-scoped
- For multi-tenant SaaS, combine Organizations with tenant-scoped database queries (
WHERE org_id = :orgId) - Session claims include
org_id,org_role, andorg_permissions-- available in middleware without API calls - Verified domains + SAML SSO enable "just-in-time provisioning" -- users auto-join the org on first SSO sign-in
- Consider the
org:sys_*system permissions (sys_memberships:manage,sys_memberships:read,sys_domains:manage) for built-in org management actions
Resources
Next Steps
Proceed to clerk-migration-deep-dive for auth provider migration.
> related_skills --same-repo
> fathom-cost-tuning
Optimize Fathom API usage and plan selection. Trigger with phrases like "fathom cost", "fathom pricing", "fathom plan".
> fathom-core-workflow-b
Sync Fathom meeting data to CRM and build automated follow-up workflows. Use when integrating Fathom with Salesforce, HubSpot, or custom CRMs, or creating automated post-meeting email summaries. Trigger with phrases like "fathom crm sync", "fathom salesforce", "fathom follow-up", "fathom post-meeting workflow".
> fathom-core-workflow-a
Build a meeting analytics pipeline with Fathom transcripts and summaries. Use when extracting insights from meetings, building CRM sync, or creating automated meeting follow-up workflows. Trigger with phrases like "fathom analytics", "fathom meeting pipeline", "fathom transcript analysis", "fathom action items sync".
> fathom-common-errors
Diagnose and fix Fathom API errors including auth failures and missing data. Use when API calls fail, transcripts are empty, or webhooks are not firing. Trigger with phrases like "fathom error", "fathom not working", "fathom api failure", "fix fathom".