Clerk Orgs

作者 clerka02dbd2a933bMIT86 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫今天更新

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.

AI 產生的概覽

指導在 B2B 多租戶應用程式中實作 Clerk Organizations:組織切換、角色、權限、邀請與企業 SSO。

功能
此技能提供為應用程式接入 Clerk Organizations 的說明:啟用組織、選擇成員模式、建立組織、透過角色與權限檢查保護路由、管理成員與邀請,以及設定企業 SSO。它附帶角色與權限、邀請、企業 SSO 和 Next.js 模式的參考文件,並提供 CLI 與後端 API 指令範例。內容也涵蓋常見錯誤特徵與注意事項,例如中繼資料覆寫和席次上限。
適用情境
當專案把團隊、工作區、租戶或公司視為客戶時使用,或當需要組織切換、RBAC、依組織劃分的路由、成員管理或依組織設定的 SSO 時使用。它也適用於判斷某個應用程式是否應採用組織功能。
執行需求
需要 Clerk 憑證(NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 和 CLERK_SECRET_KEY),並在 Clerk 控制台啟用 Organizations。此技能僅為說明文件,不含指令碼;它引用 Clerk SDK 與 CLI 指令,存取 Clerk API 和文件時可能需要網路連線。

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 the choose-organization task and disables personal accounts, while Membership optional keeps personal accounts available for B2C + B2B coexistence. Pick optional if you need personal subscriptions alongside org subscriptions.

Version: This skill targets current SDKs (@clerk/nextjs v7+, @clerk/react v6+ — Core 3). Core 2 differences are noted inline with > **Core 2 ONLY (skip if current SDK):** callouts — see clerk skill 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, Account
  • The same foreign key on most tables: workspace_id, team_id, account_id
  • Routes scoped to a tenant: /[workspace], /[slug]/settings, /t/:tenantId
  • "Invite your team", "members", "seats", or "admin" in UI copy or the README
  • A *_members join table, or hand-rolled role / permission columns
  • Per-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, or SharedWith table keyed on two user IDs
  • A role or permission column 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_id with no container above it, and nothing is shared
  • No invitation, membership, or sharing concept anywhere
  • Pricing is per person, or there is none

How to act:

What you foundWhat to do
2 or more strong signalsRecommend 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 signalRaise 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 aboveDon'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

  1. Enable Organizations — via Dashboard → Organizations settings or clerk enable orgs (see Agent-first section). Pick Membership required (B2B-only) or Membership optional (B2C + B2B).
  2. Create an org — via <OrganizationSwitcher />, <CreateOrganization />, or programmatically with clerkClient().organizations.createOrganization().
  3. Protect routes — read orgId / orgSlug from auth() and gate with has({ role }) or has({ permission }).
  4. Manage members — send invitations via Backend API or the built-in <OrganizationProfile /> tab.
  5. Cap membership — set maxAllowedMemberships at org creation or pick a seat-limited Billing Plan (see clerk-billing skill).

What Do You Need?

TaskReference
System permissions catalog, custom roles, role setsreferences/roles-permissions.md
Invitation lifecycle (create, list, revoke, built-in UI)references/invitations.md
Enterprise SSO setup, provider field access, domain verificationreferences/enterprise-sso.md
Next.js adaptations for orgs (per-page role/permission checks, slug invariants, orgId-scoped writes)references/nextjs-patterns.md

References

ReferenceDescription
references/roles-permissions.mdDefault + custom roles, System Permissions catalog, permission naming
references/invitations.mdBackend API for invitations + built-in UI
references/enterprise-sso.mdSAML/OIDC per-org, domain verification, correct field access
references/nextjs-patterns.mdNext.js adaptations specific to orgs. For generic Next.js patterns see clerk-nextjs-patterns skill.

Dashboard shortcuts

ActionURL
Enable Organizations + Membership modehttps://dashboard.clerk.com/last-active?path=organizations-settings
Manage roles + permissionshttps://dashboard.clerk.com/last-active?path=organizations-settings/roles
Create/edit an organizationhttps://dashboard.clerk.com/last-active?path=organizations
Webhooks for org eventshttps://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

bash
clerk enable orgs

For additional settings (membership cap, verified domains, admin delete), patch the instance config:

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

bash
# 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

bash
# 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

bash
# 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-billing skill.
  • Roles + permissions catalog is editable in references/roles-permissions.md. Custom role creation goes through clerk 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:

typescript
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.tsxapp/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:

typescript
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>}

3. Role-Based Access Control

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

typescript
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>

tsx
import { Show } from '@clerk/nextjs'
<Show when={{ role: 'org:admin' }}>  <AdminPanel /></Show>
<Show when={{ permission: 'org:invoices:create' }}>  <NewInvoiceButton /></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):

astro
<Show when={{ role: 'org:admin' }}>  <AdminPanel /></Show>

5. OrganizationSwitcher

tsx
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 to false. Pass true for B2B-only apps.
  • afterCreateOrganizationUrl, afterSelectOrganizationUrl, afterLeaveOrganizationUrl, afterSelectPersonalUrl — navigation hooks. :slug is 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:

tsx
import { ClerkProvider } from '@clerk/nextjs'
<ClerkProvider taskUrls={{ 'choose-organization': '/session-tasks/choose-organization' }}>  {children}</ClerkProvider>
tsx
// app/session-tasks/choose-organization/page.tsximport { 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

RoleDefault meaning
org:adminFull access — all System Permissions, can manage org + memberships
org:memberRead 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:

typescript
const { has } = await auth()
has({ plan: 'gold' })        // subscription planhas({ 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.

typescript
// 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

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

typescript
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 / symptomRoot causeFix
orgId / orgSlug is undefined for a signed-in userOrganizations not enabled for this instance, OR user has no active org (personal account)Enable in Dashboard → Organizations; check Membership mode; surface <OrganizationSwitcher />
has({ permission: ... }) always falseAn invented slug (org:manage_members), or a System Permission (org:sys_*), which has() can't checkCheck the role (has({ role: 'org:admin' })) or a custom Permission created in the Dashboard (see roles-permissions.md)
has({ role }) returns false but user looks like an adminSession token stale after role changeRe-sign-in, or refresh the session: await clerk.session?.reload()
has({ permission }) false even with the role assignedFeature 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 contextWrap in a choose-organization session-task route only; don't render unconditionally
enterpriseAccounts[0].provider is undefinedAccessing provider at the wrong nesting levelUse user.enterpriseAccounts[0].enterpriseConnection?.provider

Authorization Pattern (Complete Example)

Server component protecting a slug-scoped admin page:

typescript
import { auth } from '@clerk/nextjs/server'import { redirect } from 'next/navigation'
export default async function AdminPage({ params }: { params: Promise<{ slug: string }> }) {  const { slug } = await params  const { orgSlug, has } = await auth()
  if (orgSlug !== slug) redirect('/dashboard')  if (!has({ role: 'org:admin' })) redirect(`/orgs/${orgSlug}`)
  return <div>Admin settings for {orgSlug}</div>}

For per-page role and permission checks with auth.protect() (Next.js) see references/nextjs-patterns.md.

Invitations (short form)

Send from a server action or route handler:

typescript
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 to invite members')  }
  const clerk = await clerkClient()  return clerk.organizations.createOrganizationInvitation({    organizationId: orgId,       // the active org that has() checked    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

  1. Enable — Organizations + Membership mode in Dashboard
  2. Create org — via UI component or Backend API
  3. Invite members — Backend API or built-in UI, with inviterUserId
  4. Gate access — has({ role }), or has({ permission }) with a custom Permission. has() can't check System Permissions (org:sys_*)
  5. Scope routes — orgSlug === params.slug on every protected page
  6. 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

來源與署名

來源:clerk/skills位於skills/clerk-orgs提交a02dbd2

授權條款: MIT

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架