> 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".

fetch
$curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/clerk-enterprise-rbac?format=md"
SKILL.mdclerk-enterprise-rbac

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):

RoleKeyBuilt-in Permissions
Adminorg:adminFull org management (members, settings, billing)
Memberorg:memberView org, read-only access

Custom permissions (create in Dashboard > Organizations > Permissions):

PermissionKeyDescription
Read dataorg:data:readView organization resources
Write dataorg:data:writeCreate/update resources
Delete dataorg:data:deleteDelete resources
Manage billingorg:billing:manageAccess billing settings
View analyticsorg:analytics:readAccess analytics dashboard

Custom roles (create in Dashboard > Organizations > Roles):

RolePermissionsUse Case
org:managerdata:read, data:write, analytics:readContent managers
org:viewerdata:readRead-only stakeholders
org:billing_admindata:read, billing:manageFinance 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:

  1. ACS URL: https://<your-clerk-frontend-api>.clerk.accounts.dev/v1/saml/acs
  2. Entity ID: https://<your-clerk-frontend-api>.clerk.accounts.dev/v1/saml/metadata
  3. Upload IdP metadata XML from your provider (Okta, Azure AD, Google Workspace)
  4. 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

ErrorCauseSolution
orgId is nullNo active organizationRedirect to org selector, show <OrganizationSwitcher />
has() returns falseRole/permission not assignedCheck assignment in Dashboard > Organizations > Members
Permission denied on middlewareUser lacks required roleVerify route matcher maps to correct role
SSO login failsMisconfigured IdP metadataVerify ACS URL and Entity ID in IdP settings
Invitation failsEmail already a memberCheck membership before inviting
Custom role not visibleCreated via API, not DashboardRoles 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, and org_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.

┌ stats

installs/wk0
░░░░░░░░░░
github stars1.7K
██████████
first seenMar 23, 2026
└────────────

┌ repo

jeremylongshore/claude-code-plugins-plus-skills
by jeremylongshore
└────────────