Skillshub clerk-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/clerk-enterprise-rbac" ~/.claude/skills/comeonoliver-skillshub-clerk-enterprise-rbac && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/clerk-enterprise-rbac/SKILL.mdsource content
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 | | Full org management (members, settings, billing) |
| Member | | View org, read-only access |
Custom permissions (create in Dashboard > Organizations > Permissions):
| Permission | Key | Description |
|---|---|---|
| Read data | | View organization resources |
| Write data | | Create/update resources |
| Delete data | | Delete resources |
| Manage billing | | Access billing settings |
| View analytics | | Access analytics dashboard |
Custom roles (create in Dashboard > Organizations > Roles):
| Role | Permissions | Use Case |
|---|---|---|
| , , | Content managers |
| | Read-only stakeholders |
| , | 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
,firstNamelastName
// 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 |
|---|---|---|
is null | No active organization | Redirect to org selector, show |
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
, andorg_role
-- available in middleware without API callsorg_permissions - Verified domains + SAML SSO enable "just-in-time provisioning" -- users auto-join the org on first SSO sign-in
- Consider the
system permissions (org:sys_*
,sys_memberships:manage
,sys_memberships:read
) for built-in org management actionssys_domains:manage
Resources
Next Steps
Proceed to
clerk-migration-deep-dive for auth provider migration.