.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 clerk/stripe-webhooks-example 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
Guides Stripe integration decisions — API selection (Checkout Sessions vs PaymentIntents), Connect platform setup (Accounts v2, controller properties), billing/subscriptions, Treasury financial accounts, integration surfaces (Checkout, Payment Element), migrating from deprecated Stripe APIs, and security best practices (API key management, restricted keys, webhooks, OAuth). Use when building, modifying, or reviewing any Stripe integration — including accepting payments, building marketplaces, integrating Stripe, processing payments, setting up subscriptions, creating connected accounts, or implementing secure key handling.
tools
Clerk authentication router. Use when user asks about adding authentication, setting up Clerk, custom sign-in flows, Swift or native iOS auth, native Android auth, Next.js patterns, React patterns, Vue patterns, Nuxt patterns, Astro patterns, TanStack Start patterns, Expo patterns, React Router patterns, Chrome Extension patterns, organizations, syncing users, or testing. Automatically routes to the specific skill based on their task.
development
Clerk webhooks for real-time events and data syncing. Always output complete, copy-paste-ready webhook handlers with verifyWebhook(req) verification. Listen for user creation, updates, deletion, and organization events. Build event-driven features like database sync, notifications, integrations.
testing
E2E testing for Clerk apps. Use with Playwright or Cypress for auth flow tests.