.agents/skills/clerk-orgs/SKILL.md
Clerk Organizations for B2B SaaS - create multi-tenant apps with org switching, role-based access, verified domains, and enterprise SSO. Use for team workspaces, RBAC, org-based routing, member management.
npx skillsauth add dgminhtam/signapse-ui clerk-orgsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Prerequisite: Enable Organizations in Clerk Dashboard first.
Version: Check
package.jsonfor the SDK version — seeclerkskill for the version table. Core 2 differences are noted inline with> **Core 2 ONLY (skip if current SDK):**callouts.
| Task | Link | |------|------| | Overview | https://clerk.com/docs/guides/organizations/overview | | Org slugs in URLs | https://clerk.com/docs/guides/organizations/org-slugs-in-urls | | Roles & permissions | https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions | | Check access | https://clerk.com/docs/guides/organizations/control-access/check-access | | Invitations | https://clerk.com/docs/guides/organizations/add-members/invitations | | OrganizationSwitcher | https://clerk.com/docs/reference/components/organization/organization-switcher | | Verified domains | https://clerk.com/docs/guides/organizations/verified-domains | | Enterprise SSO | https://clerk.com/docs/guides/organizations/add-members/sso |
Server-side access to organization:
import { auth } from '@clerk/nextjs/server'
const { orgId, orgSlug } = await auth()
console.log(`Current org: ${orgSlug}`)
Create routes that accept org slug:
app/orgs/[slug]/page.tsx
app/orgs/[slug]/settings/page.tsx
Access the slug:
export default function DashboardPage({ params }: { params: { slug: string } }) {
return <div>Organization: {params.slug}</div>
}
Verify user has access to specific org:
import { auth } from '@clerk/nextjs/server'
export default async function ProtectedPage() {
const { orgId, orgSlug } = await auth()
if (!orgId) {
return <div>Not in an organization</div>
}
return <div>Welcome to {orgSlug}</div>
}
Check if user has specific role:
const { has } = await auth()
if (!has({ role: 'org:admin' })) {
return <div>Admin access required</div>
}
Create custom roles and permissions in Dashboard → Organizations → Roles. Permission format: org:resource:action.
// Server component — check a custom billing permission
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function BillingPage() {
const { has } = await auth()
if (!has({ permission: 'org:billing:manage' })) {
redirect('/unauthorized')
}
return <BillingDashboard />
}
// Middleware — protect an entire route segment
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isBillingRoute = createRouteMatcher(['/orgs/:slug/billing(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isBillingRoute(req)) {
await auth.protect({ permission: 'org:billing:manage' })
}
})
// Client component — conditional rendering
import { Show } from '@clerk/nextjs'
<Show when={{ permission: 'org:billing:manage' }}>
<BillingSettings />
</Show>
Permission naming convention: org:resource:action (e.g., org:billing:manage, org:reports:view, org:api_keys:create). Always prefix with org: for organization-scoped permissions.
Let users switch between organizations:
import { OrganizationSwitcher } from '@clerk/nextjs'
export default function Nav() {
return (
<header>
<h1>Dashboard</h1>
<OrganizationSwitcher />
</header>
)
}
All new members get assigned a role:
| Role | Permissions |
|------|-------------|
| org:admin | Full access, manage members, settings |
| org:member | Limited access, read-only |
Custom roles can be created in the dashboard.
| Permission | Role |
|-----------|------|
| org:create | Can create new organizations |
| org:manage_members | Can invite/remove members (default: admin) |
| org:manage_roles | Can change member roles (default: admin) |
| org:update_metadata | Can update org metadata (default: admin) |
Complete example protecting a route:
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function AdminPage({ params }: { params: { slug: string } }) {
const { orgSlug, has } = await auth()
// Verify user is in the org
if (orgSlug !== params.slug) {
redirect('/dashboard')
}
// Check if admin
if (!has({ role: 'org:admin' })) {
redirect(`/orgs/${orgSlug}`)
}
return <div>Admin settings for {orgSlug}</div>
}
<Show>Use <Show> for role-based conditional rendering in client components:
import { Show } from '@clerk/nextjs'
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>
<Show when={{ permission: 'org:billing:manage' }}>
<BillingSettings />
</Show>
Core 2 ONLY (skip if current SDK): Use
<Protect role="org:admin">and<Protect permission="org:billing:manage">instead of<Show>.
The has() method supports billing plan and feature checks for gating access:
const { has } = await auth()
has({ plan: 'gold' }) // Check subscription plan
has({ feature: 'widgets' }) // Check feature entitlement
Core 2 ONLY (skip if current SDK):
has()only supportsroleandpermissionparameters. Billing checks are not available.
When personal accounts are disabled, users must choose an organization after sign-in. This is handled by the choose-organization session task:
import { TaskChooseOrganization } from '@clerk/nextjs'
// Renders when user must select an org
<TaskChooseOrganization redirectUrlComplete="/dashboard" />
Core 2 ONLY (skip if current SDK): Session tasks are not available. Use
<OrganizationSwitcher>for org selection.
Organizations can use Enterprise SSO (SAML/OIDC) for member authentication:
// Strategy name for Enterprise SSO
strategy: 'enterprise_sso'
// Access enterprise accounts on user object
user.enterpriseAccounts
Core 2 ONLY (skip if current SDK): Uses
strategy: 'saml'instead ofstrategy: 'enterprise_sso', anduser.samlAccountsinstead ofuser.enterpriseAccounts.
Enterprise SSO is configured per organization in the Clerk Dashboard under Organizations > SSO Connections. Steps:
// Check if the active user authenticated via enterprise SSO
import { currentUser } from '@clerk/nextjs/server'
const user = await currentUser()
const ssoAccount = user?.enterpriseAccounts?.[0]
if (ssoAccount) {
console.log(`SSO provider: ${ssoAccount.provider}`)
console.log(`SSO domain: ${ssoAccount.emailAddress}`)
}
Key facts:
enterprise_sso (used in signIn.supportedFirstFactors)maxAllowedMembershipsPass maxAllowedMemberships when creating an org to cap the number of seats. Attempts to add members beyond the cap will return an error.
const clerk = await clerkClient()
const org = await clerk.organizations.createOrganization({
name: 'Acme Corp',
createdBy: userId,
maxAllowedMemberships: 10,
})
You can also update the cap after creation:
await clerk.organizations.updateOrganization(orgId, {
maxAllowedMemberships: 25,
})
When Clerk Billing is enabled, has({ permission: 'org:posts:edit' }) returns false if the Feature associated with that permission is not included in the organization's active Plan — even if the user has the permission assigned via their role. Billing gates permissions at the feature level. Ensure the required Feature is attached to the active Plan in Dashboard → Billing → Plans → Features before debugging role assignments.
updateOrganization({ publicMetadata: { tier: 'enterprise' } }) REPLACES all public metadata, not merges it. Read first, spread, then write.
Wrong:
await clerk.organizations.updateOrganization(orgId, {
publicMetadata: { newField: 'value' },
})
Right:
const org = await clerk.organizations.getOrganization(orgId)
await clerk.organizations.updateOrganization(orgId, {
publicMetadata: { ...org.publicMetadata, newField: 'value' },
})
The same rule applies to privateMetadata and to user metadata via clerkClient.users.updateUser.
| Symptom | Cause | Solution |
|---------|-------|----------|
| orgSlug is undefined | Not calling await auth() | Use const { orgSlug } = await auth() |
| Role check always fails | Not awaiting auth() | Add await before auth() |
| Users can access other orgs | Not checking orgSlug matches URL | Verify orgSlug === params.slug |
| Org not appearing in switcher | Organizations not enabled | Enable in Clerk Dashboard → Organizations |
| Invitations not working | Wrong role configuration | Ensure members have invite role permissions |
import { clerkClient } from '@clerk/nextjs/server'
import { auth } from '@clerk/nextjs/server'
export async function inviteMember(organizationId: string, emailAddress: string, role: string) {
const { has } = await auth()
if (!has({ permission: 'org:sys_memberships:manage' })) {
throw new Error('Not authorized to invite members')
}
const clerk = await clerkClient()
const invitation = await clerk.organizations.createOrganizationInvitation({
organizationId,
emailAddress,
role, // e.g. 'org:member' or 'org:admin'
redirectUrl: 'https://yourapp.com/accept-invite',
})
return invitation
}
const clerk = await clerkClient()
const { data: invitations } = await clerk.organizations.getOrganizationInvitationList({
organizationId,
status: ['pending'], // 'pending' | 'accepted' | 'revoked'
})
await clerk.organizations.revokeOrganizationInvitation({
organizationId,
invitationId,
requestingUserId: userId,
})
<OrganizationSwitcher /> includes a built-in member invitation UI when personal accounts are hidden:
<OrganizationSwitcher
hidePersonal
afterCreateOrganizationUrl="/orgs/:slug/dashboard"
afterSelectOrganizationUrl="/orgs/:slug/dashboard"
/>
The <OrganizationProfile /> component also provides a full members management tab with invitation and role management.
clerk-setup - Initial Clerk installclerk-webhooks - Sync org events to your databaseclerk-backend-api - Manage members programmaticallydevelopment
Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
development
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
development
React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.
tools
Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.