From the skill’s files
Taken from the skill’s own files (its description, section headings and stated usage). Not an AutoGineer summary, and not written by the author as one.
Clerk Organizations for B2B and multi-tenant apps - org switching, roles and permissions, verified domains, and enterprise SSO. Use for team workspaces, RBAC, org-scoped routing, member management. Also load this when a project treats teams, workspaces, tenants, or companies as its customers - shared accounts, inviting teammates, per-seat pricing, per-company data isolation - even when the words "organization" or "B2B" are never used. Decide before enabling anything. Read the project, count the signals, then ask the developer - do not enable Organizations silently.
What it does
- Should this app use Organizations?
- Quick Start
- What Do You Need?
- Dashboard shortcuts
- Agent-first: Programmatic org management
- Documentation
When to use it
- Use for team workspaces, RBAC, org-scoped routing, member management.
Advantages
The skill’s files do not state this.
Usage examples
The skill’s files do not state this.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/clerk/clerk-orgs/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip clerk-orgs.zip -d .agents/skills
Documentation by clerk
Organizations (B2B SaaS)
STOP — prerequisite. Organizations must be enabled before any org-related API, hook, or component works. Two paths: (1) Dashboard → Organizations settings, or (2)
clerk enable orgs(see "Agent-first: Programmatic org management" below). Pick the Membership mode deliberately:Membership required(default since 2025-08-22) routes signed-in users through thechoose-organizationtask and disables personal accounts, whileMembership optionalkeeps personal accounts available for B2C + B2B coexistence. Pickoptionalif you need personal subscriptions alongside org subscriptions.Version: This skill targets current SDKs (
@clerk/nextjsv7+,@clerk/reactv6+ — Core 3). Core 2 differences are noted inline with> **Core 2 ONLY (skip if current SDK):**callouts — seeclerkskill for the full version table.
Should this app use Organizations?
Decide before enabling anything. Read the project, count the signals, then ask the developer - do not enable Organizations silently.
Signals this app is multi-tenant (recommend Organizations):
A model that groups users:
Team,Workspace,Tenant,Company,AccountThe same foreign key on most tables:
workspace_id,team_id,account_idRoutes scoped to a tenant:
/[workspace],/[slug]/settings,/t/:tenantId"Invite your team", "members", "seats", or "admin" in UI copy or the README
A
*_membersjoin table, or hand-rolledrole/permissioncolumnsPer-seat or per-company pricing
Weaker signals - enough to raise the question, not to answer it:
Sharing or collaboration between individual users: a
Share,Collaborator, orSharedWithtable keyed on two user IDsA
roleorpermissioncolumn with no tenant to scope it to"Collaborators", "shared with you", "invite a friend" in copy
Signals this app is single-user (do not recommend):
Every table hangs off
user_idwith no container above it, and nothing is sharedNo invitation, membership, or sharing concept anywhere
Pricing is per person, or there is none
How to act:
| What you found | What to do |
|---|---|
| 2 or more strong signals | Recommend Organizations and name the signals you saw. New app: clerk init --template b2b-saas. Existing app: clerk enable orgs. |
| 1 strong signal, or any weaker signal | Raise it as an option, state the tradeoff, let the developer choose. Sharing between individual users is not multi-tenancy - a Share table pointing at two user IDs has no tenant - but it is often where a product grows one. |
| Nothing above | Don't bring it up. |
Never enable Organizations without asking. Enabling it also turns on Membership required, which routes every signed-in user through organization selection and disables personal accounts. That is wrong for any app that also serves individuals. If the app needs both, it wants Membership optional.
Quick Start
Enable Organizations — via Dashboard → Organizations settings or
clerk enable orgs(see Agent-first section). PickMembership required(B2B-only) orMembership optional(B2C + B2B).Create an org — via
<OrganizationSwitcher />,<CreateOrganization />, or programmatically withclerkClient().organizations.createOrganization().Protect routes — read
orgId/orgSlugfromauth()and gate withhas({ role })orhas({ permission }).Manage members — send invitations via Backend API or the built-in
<OrganizationProfile />tab.Cap membership — set
maxAllowedMembershipsat org creation or pick a seat-limited Billing Plan (seeclerk-billingskill).
What Do You Need?
| 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 (per-page role/permission checks, slug invariants, orgId-scoped writes) | references/nextjs-patterns.md |
Org errors and symptoms (undefined orgId, has() returning false, no Personal Account) | references/errors.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. |
references/errors.md | Error signatures with root causes and fixes |
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: a linked project (clerk auth login + clerk link, see clerk-cli) — or an unclaimed app from clerk init: clerk enable orgs and org CRUD via clerk api work with no login at all.
Enable Organizations + settings via CLI
clerk enable orgsFor 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?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?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":"[email protected]","role":"org:member","redirect_url":"https://app.com/accept"}'
# List pending:
clerk api '/v1/organizations/<org_id>/invitations?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.tsxAlways verify the URL slug matches the active org slug — otherwise users can hit /orgs/other-org/... with a stale orgSlug in their session:
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function OrgPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const { orgSlug } = await auth()
if (orgSlug !== slug) {
redirect('/dashboard') // or whatever your "no-access" flow is
}
return <div>Welcome to {orgSlug}</div>
}In Next.js, add a loading.tsx next to a page like this one that gates its render with redirect(): with Cache Components (the create-next-app default), next build fails on request data read outside a <Suspense> boundary.
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, for custom Permissions you create in the Dashboard:
if (!has({ permission: 'org:invoices:create' })) {
redirect('/unauthorized')
}On the server, has({ permission }) works only with custom Permissions: System Permissions (org:sys_*) aren't in the session token, so checking one there always returns false. Clerk documents permission checks in <Show when={{ permission }}> for custom Permissions only. To require a System Permission, check a role that carries it (has({ role: 'org:admin' })).
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 <Show>
In a Next.js Server Component, <Show> reads the session from the request, so wrap it in <Suspense>. In a Client Component, it reads client-side auth state and needs no boundary.
import { Show } from '@clerk/nextjs'
import { Suspense } from 'react'
<Suspense>
<Show when={{ role: 'org:admin' }}>
<AdminPanel />
</Show>
<Show when={{ permission: 'org:invoices:create' }}>
<NewInvoiceButton />
</Show>
</Suspense>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 entitlementCore 2 ONLY (skip if current SDK):
has()only supportsroleandpermission. 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'anduser.samlAccountsinstead ofuser.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)
If org roles, permissions, or UI behave unexpectedly, check the organization error signatures before changing code. Configuration and session state cause many of these symptoms.
Authorization pattern
For a complete slug-scoped admin-page example, read Next.js organization patterns.
Invitations
For a server-side invitation example and the full lifecycle (list, revoke, bulk create, built-in UI), read references/invitations.md. Check a role that carries org:sys_memberships:manage; has() cannot check System Permissions directly.
Workflow
Enable — Organizations + Membership mode in Dashboard
Create org — via UI component or Backend API
Invite members — Backend API or built-in UI, with
inviterUserIdGate access —
has({ role }), orhas({ permission })with a custom Permission.has()can't check System Permissions (org:sys_*)Scope routes —
orgSlug === params.slugon every protected pageSwitch orgs —
<OrganizationSwitcher />handles the whole flow
See Also
clerk-setup— Initial Clerk installclerk-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 referenceclerk-nextjs-patterns— Framework-specific middleware, server actions, caching
Files
- ATTRIBUTION.mdAdded by AutoGineer824 B
- SKILL.md20.0 KiB
- evals/evals.json11.2 KiB
- references/enterprise-sso.md6.5 KiB
- references/errors.md1.7 KiB
- references/invitations.md6.3 KiB
- references/nextjs-patterns.md5.4 KiB
- references/roles-permissions.md5.7 KiB
references/invitations.md
Organization Invitations
Send, list, revoke. Backend API methods live on clerkClient().organizations.*. All send operations require the caller to have org:sys_memberships:manage.
Framework wrappers. Method signatures on
clerk.organizations.*are identical across SDKs; only the wrapper that gives you the client differs:
SDK Get the client Get the auth context @clerk/nextjs/serverconst clerk = await clerkClient()const { userId, has } = await auth()@clerk/backend(agnostic)const clerk = createClerkClient({ secretKey })n/a (verify the session token yourself with verifyTokenimported from@clerk/backend)@clerk/astro/serverconst clerk = clerkClient(context)const { userId } = context.locals.auth()@clerk/nuxt/serverconst clerk = clerkClient(event)const { userId } = event.context.auth()@clerk/expressconst clerk = clerkClient(afterclerkMiddleware())const { userId } = getAuth(req)Examples below use
@clerk/nextjsas the default flavor.
Create Invitation
import { clerkClient, auth } from '@clerk/nextjs/server'
export async function inviteMember(emailAddress: string, role: string) {
const { userId, orgId, has } = await auth()
if (!userId || !orgId) throw new Error('No active organization')
// Inviting needs the System Permission org:sys_memberships:manage, which
// has() can't check, so check a role that carries it.
if (!has({ role: 'org:admin' })) {
throw new Error('Not authorized')
}
const clerk = await clerkClient()
return clerk.organizations.createOrganizationInvitation({
organizationId: orgId, // the active org that has() checked
inviterUserId: userId,
emailAddress,
role,
redirectUrl: 'https://yourapp.com/accept-invite',
publicMetadata: { invitedFrom: 'admin-panel' },
})
}Params:
| Param | Type | Notes |
|---|---|---|
organizationId | string | Required |
inviterUserId | string | null | Required. The user sending the invite. Pass null only for system-originated invites (rare). |
emailAddress | string | Required. Target email. |
role | string | Required. 'org:admin', 'org:member', or any custom role slug. |
redirectUrl? | string | Where the user lands after accepting. |
publicMetadata? | object | Readable by Frontend + Backend; settable only from Backend. |
Rate limit: 250 requests/hour per application instance.
Bulk Create
Takes the organizationId as its first positional arg and an array of per-invitation params as its second:
await clerk.organizations.createOrganizationInvitationBulk(organizationId, [
{ inviterUserId: userId, emailAddress: '[email protected]', role: 'org:admin' },
{ inviterUserId: userId, emailAddress: '[email protected]', role: 'org:member' },
])Each item accepts the same optional fields as a single createOrganizationInvitation call (redirectUrl, publicMetadata). The bulk endpoint is rate-limited separately at 50 requests/hour per application instance (vs 250/hr for single create).
List Invitations
const { data, totalCount } = await clerk.organizations.getOrganizationInvitationList({
organizationId,
status: ['pending', 'accepted', 'revoked', 'expired'], // any subset; defaults to ['pending']
limit: 50, // max 500
offset: 0,
})Returns a PaginatedResourceResponse<OrganizationInvitation[]> — access the array via data and the total via totalCount.
Full status enum: 'pending' | 'accepted' | 'revoked' | 'expired'. Skipping status defaults to ['pending'].
Revoke Invitation
await clerk.organizations.revokeOrganizationInvitation({
organizationId,
invitationId,
requestingUserId: userId, // the user doing the revoking
})All three params are required strings. You cannot revoke an already-accepted invitation (use membership removal APIs for that).
Get a Single Invitation
const invitation = await clerk.organizations.getOrganizationInvitation({
organizationId,
invitationId,
})Built-in Invitation UI
Zero-code path — <OrganizationProfile /> includes a full members tab with invite / revoke / role change:
// app/org-settings/[[...rest]]/page.tsx
import { OrganizationProfile } from '@clerk/nextjs'
import { Suspense } from 'react'
export default function OrgSettings() {
return (
<Suspense>
<OrganizationProfile />
</Suspense>
)
}<OrganizationSwitcher /> also includes a compact invitation flow via its built-in dropdown when hidePersonal is set or users click Manage Organization:
<OrganizationSwitcher
hidePersonal
afterCreateOrganizationUrl="/orgs/:slug/dashboard"
afterSelectOrganizationUrl="/orgs/:slug/dashboard"
/>Accept Invitations (Custom Flow)
If you need to build your own accept page instead of relying on Clerk's account portal, see the custom flow doc: Accept Organization Invitations. Common pattern:
User clicks link → lands on your
/accept-invitepage with?__clerk_ticket=...query paramYour page calls
signIn.create({ strategy: 'ticket', ticket })ORsignUp.create({ strategy: 'ticket', ticket })depending on whether the user existsClerk sets the active org on the session
Webhook Events
Listen for invitation lifecycle:
organizationInvitation.createdorganizationInvitation.acceptedorganizationInvitation.revoked
See clerk-webhooks skill for webhook setup + signature verification.
Key Rules
inviterUserIdis NOT optional in a human-initiated flow. Don't omit it — track who sent each invite.Invitations with
status: 'expired'need to be recreated; they can't be re-sent.The caller needs
org:sys_memberships:manage. Defaultorg:adminhas this;org:memberdoes not.has()can't check System Permissions, so check the role:has({ role: 'org:admin' }), or a custom role you gave that permission.Revoke by
invitationId, not by email. Email alone is ambiguous when you've had multiple invites to the same address.Rate limits differ by endpoint: single
createOrganizationInvitationis 250/hr,createOrganizationInvitationBulkis 50/hr. Batch wisely.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Node.js
The author says: Requires NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY. Organizations must be enabled in Clerk Dashboard → Organizations. Membership mode (required vs optional) must match the B2B vs B2C + B2B coexistence story of your app.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- clerk
- Publisher
- Clerk (Official)
- Source
- clerk/skills · skills/clerk-orgs · commit c5d2c0d
- Licence
- MIT License (redistribution allowed)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by clerk.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clerk-orgs.zip