SKILL.md
$2a
Task
Reference
System permissions catalog, custom roles, role sets
references/roles-permissions.md
Invitation lifecycle (create, list, revoke, built-in UI)
references/invitations.md
Enterprise SSO setup, provider field access, domain verification
references/enterprise-sso.md
Next.js adaptations for orgs (role/permission middleware, slug invariants, orgId-scoped writes)
references/nextjs-patterns.md
References
Reference
Description
references/roles-permissions.md
Default + custom roles, System Permissions catalog, permission naming
references/invitations.md
Backend API for invitations + built-in UI
references/enterprise-sso.md
SAML/OIDC per-org, domain verification, correct field access
references/nextjs-patterns.md
Next.js adaptations specific to orgs. For generic Next.js patterns see clerk-nextjs-patterns skill.
Dashboard shortcuts
Action
URL
Enable Organizations + Membership mode
https://dashboard.clerk.com/last-active?path=organizations-settings
Manage roles + permissions
https://dashboard.clerk.com/last-active?path=organizations-settings/roles
Create/edit an organization
https://dashboard.clerk.com/last-active?path=organizations
Webhooks for org events
https://dashboard.clerk.com/last-active?path=webhooks
Agent-first: Programmatic org management
Org settings (enable toggle, membership cap, admin delete, domains) are patchable via PLAPI Instance Config. Org CRUD + memberships + invitations live in BAPI. Useful for agents seeding orgs, replicating settings across instances, or version-controlling org structure.
Pre-req: project linked (clerk auth login + clerk link, see clerk-setup).
Enable Organizations + settings via CLI
clerk enable orgs
For additional settings (membership cap, verified domains, admin delete), patch the instance config:
clerk api --platform PATCH /v1/platform/applications/<app_id>/instances/<ins_id>/config \
-d '{"organization_settings":{"max_allowed_memberships":50,"domains_enabled":true,"admin_delete_enabled":true}}'
Create / list / delete orgs (BAPI)
# Create:
clerk api -X POST /v1/organizations \
-d '{"name":"Acme","slug":"acme","created_by":"user_xxx","max_allowed_memberships":10}'
# List:
clerk api /v1/organizations --query 'limit=20'
# Get one:
clerk api /v1/organizations/<org_id>
# Update:
clerk api -X PATCH /v1/organizations/<org_id> -d '{"name":"Acme Inc."}'
# Delete:
clerk api -X DELETE /v1/organizations/<org_id>
Memberships
# Add a user to an org:
clerk api -X POST /v1/organizations/<org_id>/memberships \
-d '{"user_id":"user_xxx","role":"org:admin"}'
# List members:
clerk api /v1/organizations/<org_id>/memberships --query 'limit=50'
# Update role:
clerk api -X PATCH /v1/organizations/<org_id>/memberships/<user_id> \
-d '{"role":"org:member"}'
# Remove:
clerk api -X DELETE /v1/organizations/<org_id>/memberships/<user_id>
Invitations
# Send:
clerk api -X POST /v1/organizations/<org_id>/invitations \
-d '{"email_address":"alice@example.com","role":"org:member","redirect_url":"https://app.com/accept"}'
# List pending:
clerk api /v1/organizations/<org_id>/invitations --query 'status=pending'
# Revoke:
clerk api -X POST /v1/organizations/<org_id>/invitations/<inv_id>/revoke \
-d '{"requesting_user_id":"user_xxx"}'
Notes
- This handles org config + CRUD. Subscription / billing for orgs (org plans, seat-limit pricing) flows through
clerk-billingskill.
- Roles + permissions catalog is editable in
references/roles-permissions.md. Custom role creation goes throughclerk config patch(instance-level role definitions) — see Dashboard's role editor for the UX equivalent.
- For SSO / verified domain provisioning, see
references/enterprise-sso.md.
Documentation
Key Patterns
Examples use @clerk/nextjs by default. For other frameworks swap the import to @clerk/react (Vite/CRA), @clerk/astro/components, @clerk/vue, @clerk/expo, @clerk/react-router, or @clerk/tanstack-react-start — the feature-level APIs (has(), orgId, <OrganizationSwitcher />, <Show>) are identical across SDKs. Framework-specific patterns (middleware, redirects) live in references/nextjs-patterns.md.
1. Read Organization from Auth
Server-side access to active organization:
import { auth } from '@clerk/nextjs/server'
const { orgId, orgSlug, orgRole } = await auth()
if (!orgId) {
// user has no active org — either not in any, or viewing Personal Account
}
auth() is Next.js-specific. Equivalent server-side accessors per SDK: auth(event) (Nuxt via event.context.auth()), context.locals.auth() (Astro), getAuth(req) (Express, after clerkMiddleware()). Client-side: useAuth() (React-based SDKs) or composables (Vue/Nuxt). All return the same orgId / orgSlug / orgRole shape.
2. Dynamic Routes with Org Slug
Route-per-org pattern works in any framework supporting file-based dynamic routes. Next.js example:
app/orgs/[slug]/page.tsx
app/orgs/[slug]/settings/page.tsx
Always verify the URL slug matches the active org slug — otherwise users can hit /orgs/other-org/... with a stale orgSlug in their session:
export default async function OrgPage({ params }: { params: { slug: string } }) {
const { orgSlug } = await auth()
if (orgSlug !== params.slug) {
redirect('/dashboard') // or whatever your "no-access" flow is
}
return <div>Welcome to {orgSlug}</div>
}
3. Role-Based Access Control
const { has } = await auth()
if (!has({ role: 'org:admin' })) {
return <div>Admin access required</div>
}
Permission checks use the same has() surface:
if (!has({ permission: 'org:sys_memberships:manage' })) {
redirect('/unauthorized')
}
Permission naming convention. System Permissions prefix with org:sys_; custom Permissions use org:<resource>:<action>. The full System Permissions catalog lives in references/roles-permissions.md — the short list is:
org:sys_memberships:{read, manage}
org:sys_profile:{manage, delete}
org:sys_domains:{read, manage}
org:sys_billing:{read, manage}
Do NOT invent names like org:create, org:manage_members, org:update_metadata — those are not real permission slugs. See references/roles-permissions.md for custom roles and the permission table.
4. Conditional Rendering with
import { Show } from '@clerk/nextjs'
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>
<Show when={{ permission: 'org:sys_memberships:manage' }}>
<MembersTab />
</Show>
Core 2 ONLY (skip if current SDK): Use <Protect role="org:admin"> / <Protect permission="..."> instead of <Show>. <Show> replaced both <Protect> and <SignedIn>/<SignedOut> in Core 3.
Astro template syntax for the same component (imported from @clerk/astro/components):
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>
5. OrganizationSwitcher
import { OrganizationSwitcher } from '@clerk/nextjs'
<OrganizationSwitcher
hidePersonal
afterCreateOrganizationUrl="/orgs/:slug/dashboard"
afterSelectOrganizationUrl="/orgs/:slug/dashboard"
/>
Key props:
hidePersonal: boolean— hide the Personal Account option. Defaults tofalse. Passtruefor B2B-only apps.
afterCreateOrganizationUrl,afterSelectOrganizationUrl,afterLeaveOrganizationUrl,afterSelectPersonalUrl— navigation hooks.:slugis substituted at runtime.
createOrganizationMode,organizationProfileMode—'modal' | 'navigation'(default'modal').
The full prop list lives in the component reference.
6. Session Task — Choose Organization
When Membership required is enabled (the default), users without an org are routed through a choose-organization session task after sign-in. Clerk handles this automatically inside <SignIn />, but you can host the UI yourself:
import { ClerkProvider } from '@clerk/nextjs'
<ClerkProvider taskUrls={{ 'choose-organization': '/session-tasks/choose-organization' }}>
{children}
</ClerkProvider>
// app/session-tasks/choose-organization/page.tsx
import { TaskChooseOrganization } from '@clerk/nextjs'
export default function Page() {
return <TaskChooseOrganization redirectUrlComplete="/dashboard" />
}
TaskChooseOrganization ships as an imported component in the React-based SDKs (@clerk/nextjs, @clerk/react, @clerk/react-router, @clerk/tanstack-react-start). For the JS Frontend SDK (@clerk/clerk-js) the equivalent is clerk.mountTaskChooseOrganization(node) / clerk.unmountTaskChooseOrganization(node).
Core 2 ONLY (skip if current SDK): Session tasks aren't available. Force an org selection at sign-in by redirecting to a page that renders <OrganizationSwitcher hidePersonal />.
Default Roles + System Permissions
Role
Default meaning
org:admin
Full access — all System Permissions, can manage org + memberships
org:member
Read members + Read billing Permissions only
You can create up to 10 custom roles per instance in Dashboard → Organizations → Roles & Permissions. Role-per-org is controlled via Role Sets — see references/roles-permissions.md for the full model (custom roles, Creator/Default role settings, role sets, and the System Permissions catalog).
Billing Checks
has() also supports plan and feature checks when Clerk Billing is enabled:
const { has } = await auth()
has({ plan: 'gold' }) // subscription plan
has({ feature: 'widgets' }) // feature entitlement
Core 2 ONLY (skip if current SDK): has() only supports role and permission. Billing checks aren't available.
See clerk-billing for the full Billing surface and seat-limit plan model.
Enterprise SSO
Per-org SAML/OIDC. Configured in Dashboard → Configure → Enterprise Connections (or per-org: Organizations → select org → SSO Connections). The SSO connection owns its domain directly; no separate Verified Domain is required (and the two features are mutually exclusive on the same domain). Auto-join on first SSO sign-in uses JIT Provisioning, not Verified Domains. Key fact: the provider field lives on enterpriseConnection, not on enterpriseAccounts[0] directly. See references/enterprise-sso.md for the full flow and correct field access.
// Strategy name for Enterprise SSO (Core 3)
strategy: 'enterprise_sso'
Core 2 ONLY (skip if current SDK): Uses strategy: 'saml' and user.samlAccounts instead of user.enterpriseAccounts.
Gotchas
maxAllowedMemberships caps seats
const clerk = await clerkClient()
await clerk.organizations.createOrganization({
name: 'Acme Corp',
createdBy: userId,
maxAllowedMemberships: 10,
})
// Update later:
await clerk.organizations.updateOrganization(orgId, {
maxAllowedMemberships: 25,
})
For tier-based seat limits tied to a subscription, use a seat-limited Billing Plan (see clerk-billing).
Billing gates Permissions at the Feature level
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. Ensure the Feature is attached to the active Plan in Dashboard → Billing → Plans → Features.
Metadata updates REPLACE, not merge
updateOrganization({ publicMetadata }) overwrites all public metadata. Read first, spread, then write:
const org = await clerk.organizations.getOrganization({ organizationId: orgId })
await clerk.organizations.updateOrganization(orgId, {
publicMetadata: { ...org.publicMetadata, newField: 'value' },
})
Applies identically to privateMetadata and to user metadata via clerkClient.users.updateUser.
Error Signatures (diagnose fast)
Most "org-related" failures are configuration, not code. Do not edit components before checking these:
Error / symptom
Root cause
Fix
orgId / orgSlug is undefined for a signed-in user
Organizations not enabled for this instance, OR user has no active org (personal account)
Enable in Dashboard → Organizations; check Membership mode; surface <OrganizationSwitcher />
has({ permission: 'org:manage_members' }) always false
Using an invented permission slug
Use org:sys_memberships:manage (see roles-permissions.md catalog)
has({ role }) returns false but user looks like an admin
Session token stale after role change
Re-sign-in, or refresh the session: await clerk.session?.reload()
has({ permission }) false even with the role assigned
Feature not attached to active Plan (Billing gates permissions)
Dashboard → Billing → Plans → attach Feature
<OrganizationSwitcher /> doesn't show "Personal Account"
Membership required mode is on (the default since Aug 22, 2025)
Dashboard → Organizations settings → Membership optional
TaskChooseOrganization throws "cannot render when a user doesn't have current session tasks"
Rendered outside a choose-organization task context
Wrap in a choose-organization session-task route only; don't render unconditionally
enterpriseAccounts[0].provider is undefined
Accessing provider at the wrong nesting level
Use user.enterpriseAccounts[0].enterpriseConnection?.provider
Authorization Pattern (Complete Example)
Server component protecting a slug-scoped admin page:
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()
if (orgSlug !== params.slug) redirect('/dashboard')
if (!has({ role: 'org:admin' })) redirect(`/orgs/${orgSlug}`)
return <div>Admin settings for {orgSlug}</div>
}
For middleware-level protection (Next.js) see references/nextjs-patterns.md.
Invitations (short form)
Send from a server action or route handler:
import { clerkClient, auth } from '@clerk/nextjs/server'
export async function inviteMember(organizationId: string, emailAddress: string, role: string) {
const { userId, has } = await auth()
if (!userId) throw new Error('Not signed in')
if (!has({ permission: 'org:sys_memberships:manage' })) {
throw new Error('Not authorized to invite members')
}
const clerk = await clerkClient()
return clerk.organizations.createOrganizationInvitation({
organizationId,
inviterUserId: userId, // required per Backend API
emailAddress,
role, // e.g. 'org:admin' or 'org:member'
redirectUrl: 'https://yourapp.com/accept-invite',
})
}
The full lifecycle (list, revoke, bulk create, built-in <OrganizationProfile /> UI) lives in references/invitations.md.
Workflow
- Enable — Organizations + Membership mode in Dashboard
- Create org — via UI component or Backend API
- Invite members — Backend API or built-in UI, with
inviterUserId
- Gate access —
has({ role })/has({ permission })with canonicalorg:sys_*names
- Scope routes —
orgSlug === params.slugon every protected page
- Switch orgs —
<OrganizationSwitcher />handles the whole flow
See Also
clerk-setup— Initial Clerk install
clerk-billing— Seat-limit plans, per-plan billing,has({ plan })/has({ feature })
clerk-webhooks— Sync org events to your database (organization.created,organizationMembership.*)
clerk-backend-api— Full Backend API reference
clerk-nextjs-patterns— Framework-specific middleware, server actions, caching