← ClerkCONTENT HISTORY

Update to Clerk

Snapshot Sep 30, 2026 · 23:09 UTC · version 0.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "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.",
  "included_files": [
    {
      "relative_path": "evals/evals.json",
      "size_in_bytes": 8287
    },
    {
      "relative_path": "references/enterprise-sso.md",
      "size_in_bytes": 6688
    },
    {
      "relative_path": "references/invitations.md",
      "size_in_bytes": 6024
    },
    {
      "relative_path": "references/nextjs-patterns.md",
      "size_in_bytes": 4008
    },
    {
      "relative_path": "references/roles-permissions.md",
      "size_in_bytes": 5606
    }
  ],
  "name": "clerk-orgs",
  "skill_md_contents": "---\nname: clerk-orgs\ndescription: Clerk Organizations for B2B SaaS - create multi-tenant apps with org\n  switching, role-based access, verified domains, and enterprise SSO. Use for team\n  workspaces, RBAC, org-based routing, member management.\nallowed-tools: WebFetch\nlicense: MIT\ncompatibility: 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.\nmetadata:\n  author: clerk\n  version: 3.1.0\n---\n\n# Organizations (B2B SaaS)\n\n> **STOP — prerequisite.** Organizations must be enabled before any org-related API, hook, or component works. Two paths: (1) [Dashboard → Organizations settings](https://dashboard.clerk.com/last-active?path=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.\n>\n> **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.\n\n## Quick Start\n\n1. **Enable Organizations** — via [Dashboard → Organizations settings](https://dashboard.clerk.com/last-active?path=organizations-settings) or `clerk enable orgs` (see Agent-first section). Pick `Membership required` (B2B-only) or `Membership optional` (B2C + B2B).\n2. **Create an org** — via `<OrganizationSwitcher />`, `<CreateOrganization />`, or programmatically with `clerkClient().organizations.createOrganization()`.\n3. **Protect routes** — read `orgId` / `orgSlug` from `auth()` and gate with `has({ role })` or `has({ permission })`.\n4. **Manage members** — send invitations via Backend API or the built-in `<OrganizationProfile />` tab.\n5. **Cap membership** — set `maxAllowedMemberships` at org creation or pick a seat-limited Billing Plan (see `clerk-billing` skill).\n\n## What Do You Need?\n\n| Task | Reference |\n|------|-----------|\n| System permissions catalog, custom roles, role sets | references/roles-permissions.md |\n| Invitation lifecycle (create, list, revoke, built-in UI) | references/invitations.md |\n| Enterprise SSO setup, provider field access, domain verification | references/enterprise-sso.md |\n| Next.js adaptations for orgs (role/permission middleware, slug invariants, orgId-scoped writes) | references/nextjs-patterns.md |\n\n## References\n\n| Reference | Description |\n|-----------|-------------|\n| `references/roles-permissions.md` | Default + custom roles, System Permissions catalog, permission naming |\n| `references/invitations.md` | Backend API for invitations + built-in UI |\n| `references/enterprise-sso.md` | SAML/OIDC per-org, domain verification, correct field access |\n| `references/nextjs-patterns.md` | Next.js adaptations specific to orgs. For generic Next.js patterns see `clerk-nextjs-patterns` skill. |\n\n## Dashboard shortcuts\n\n| Action | URL |\n|---|---|\n| Enable Organizations + Membership mode | `https://dashboard.clerk.com/last-active?path=organizations-settings` |\n| Manage roles + permissions | `https://dashboard.clerk.com/last-active?path=organizations-settings/roles` |\n| Create/edit an organization | `https://dashboard.clerk.com/last-active?path=organizations` |\n| Webhooks for org events | `https://dashboard.clerk.com/last-active?path=webhooks` |\n\n## Agent-first: Programmatic org management\n\nOrg 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.\n\nPre-req: a linked project (`clerk auth login` + `clerk link`, see `clerk-setup`) — or an unclaimed app from `clerk init`: `clerk enable orgs` and org CRUD via `clerk api` work with no login at all.\n\n### Enable Organizations + settings via CLI\n\n```bash\nclerk enable orgs\n```\n\nFor additional settings (membership cap, verified domains, admin delete), patch the instance config:\n\n```bash\nclerk api --platform PATCH /v1/platform/applications/<app_id>/instances/<ins_id>/config \\\n  -d '{\"organization_settings\":{\"max_allowed_memberships\":50,\"domains_enabled\":true,\"admin_delete_enabled\":true}}'\n```\n\n### Create / list / delete orgs (BAPI)\n\n```bash\n# Create:\nclerk api -X POST /v1/organizations \\\n  -d '{\"name\":\"Acme\",\"slug\":\"acme\",\"created_by\":\"user_xxx\",\"max_allowed_memberships\":10}'\n\n# List:\nclerk api /v1/organizations --query 'limit=20'\n\n# Get one:\nclerk api /v1/organizations/<org_id>\n\n# Update:\nclerk api -X PATCH /v1/organizations/<org_id> -d '{\"name\":\"Acme Inc.\"}'\n\n# Delete:\nclerk api -X DELETE /v1/organizations/<org_id>\n```\n\n### Memberships\n\n```bash\n# Add a user to an org:\nclerk api -X POST /v1/organizations/<org_id>/memberships \\\n  -d '{\"user_id\":\"user_xxx\",\"role\":\"org:admin\"}'\n\n# List members:\nclerk api /v1/organizations/<org_id>/memberships --query 'limit=50'\n\n# Update role:\nclerk api -X PATCH /v1/organizations/<org_id>/memberships/<user_id> \\\n  -d '{\"role\":\"org:member\"}'\n\n# Remove:\nclerk api -X DELETE /v1/organizations/<org_id>/memberships/<user_id>\n```\n\n### Invitations\n\n```bash\n# Send:\nclerk api -X POST /v1/organizations/<org_id>/invitations \\\n  -d '{\"email_address\":\"alice@example.com\",\"role\":\"org:member\",\"redirect_url\":\"https://app.com/accept\"}'\n\n# List pending:\nclerk api /v1/organizations/<org_id>/invitations --query 'status=pending'\n\n# Revoke:\nclerk api -X POST /v1/organizations/<org_id>/invitations/<inv_id>/revoke \\\n  -d '{\"requesting_user_id\":\"user_xxx\"}'\n```\n\n### Notes\n\n- This handles **org config + CRUD**. Subscription / billing for orgs (org plans, seat-limit pricing) flows through `clerk-billing` skill.\n- 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.\n- For SSO / verified domain provisioning, see `references/enterprise-sso.md`.\n\n## Documentation\n\n- [Overview](https://clerk.com/docs/guides/organizations/overview)\n- [Configure + enable](https://clerk.com/docs/guides/organizations/configure)\n- [Roles and permissions](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions)\n- [Check access](https://clerk.com/docs/guides/organizations/control-access/check-access)\n- [Invitations](https://clerk.com/docs/guides/organizations/add-members/invitations)\n- [OrganizationSwitcher](https://clerk.com/docs/reference/components/organization/organization-switcher)\n- [Verified domains](https://clerk.com/docs/guides/organizations/add-members/verified-domains)\n- [Enterprise SSO](https://clerk.com/docs/guides/organizations/add-members/sso)\n\n## Key Patterns\n\nExamples 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`.\n\n### 1. Read Organization from Auth\n\nServer-side access to active organization:\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\n\nconst { orgId, orgSlug, orgRole } = await auth()\nif (!orgId) {\n  // user has no active org — either not in any, or viewing Personal Account\n}\n```\n\n`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.\n\n### 2. Dynamic Routes with Org Slug\n\nRoute-per-org pattern works in any framework supporting file-based dynamic routes. Next.js example:\n\n```\napp/orgs/[slug]/page.tsx\napp/orgs/[slug]/settings/page.tsx\n```\n\nAlways verify the URL slug matches the active org slug — otherwise users can hit `/orgs/other-org/...` with a stale `orgSlug` in their session:\n\n```typescript\nexport default async function OrgPage({ params }: { params: { slug: string } }) {\n  const { orgSlug } = await auth()\n  if (orgSlug !== params.slug) {\n    redirect('/dashboard')  // or whatever your \"no-access\" flow is\n  }\n  return <div>Welcome to {orgSlug}</div>\n}\n```\n\n### 3. Role-Based Access Control\n\n```typescript\nconst { has } = await auth()\n\nif (!has({ role: 'org:admin' })) {\n  return <div>Admin access required</div>\n}\n```\n\nPermission checks use the same `has()` surface:\n\n```typescript\nif (!has({ permission: 'org:sys_memberships:manage' })) {\n  redirect('/unauthorized')\n}\n```\n\n**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:\n\n- `org:sys_memberships:{read, manage}`\n- `org:sys_profile:{manage, delete}`\n- `org:sys_domains:{read, manage}`\n- `org:sys_billing:{read, manage}`\n\nDo 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.\n\n### 4. Conditional Rendering with `<Show>`\n\n```tsx\nimport { Show } from '@clerk/nextjs'\n\n<Show when={{ role: 'org:admin' }}>\n  <AdminPanel />\n</Show>\n\n<Show when={{ permission: 'org:sys_memberships:manage' }}>\n  <MembersTab />\n</Show>\n```\n\n> **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.\n\nAstro template syntax for the same component (imported from `@clerk/astro/components`):\n\n```astro\n<Show when={{ role: 'org:admin' }}>\n  <AdminPanel />\n</Show>\n```\n\n### 5. OrganizationSwitcher\n\n```tsx\nimport { OrganizationSwitcher } from '@clerk/nextjs'\n\n<OrganizationSwitcher\n  hidePersonal\n  afterCreateOrganizationUrl=\"/orgs/:slug/dashboard\"\n  afterSelectOrganizationUrl=\"/orgs/:slug/dashboard\"\n/>\n```\n\nKey props:\n- `hidePersonal: boolean` — hide the Personal Account option. Defaults to `false`. Pass `true` for B2B-only apps.\n- `afterCreateOrganizationUrl`, `afterSelectOrganizationUrl`, `afterLeaveOrganizationUrl`, `afterSelectPersonalUrl` — navigation hooks. `:slug` is substituted at runtime.\n- `createOrganizationMode`, `organizationProfileMode` — `'modal' | 'navigation'` (default `'modal'`).\n\nThe full prop list lives in the [component reference](https://clerk.com/docs/reference/components/organization/organization-switcher).\n\n### 6. Session Task — Choose Organization\n\nWhen `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:\n\n```tsx\nimport { ClerkProvider } from '@clerk/nextjs'\n\n<ClerkProvider taskUrls={{ 'choose-organization': '/session-tasks/choose-organization' }}>\n  {children}\n</ClerkProvider>\n```\n\n```tsx\n// app/session-tasks/choose-organization/page.tsx\nimport { TaskChooseOrganization } from '@clerk/nextjs'\n\nexport default function Page() {\n  return <TaskChooseOrganization redirectUrlComplete=\"/dashboard\" />\n}\n```\n\n`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)`.\n\n> **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 />`.\n\n## Default Roles + System Permissions\n\n| Role | Default meaning |\n|------|-------------|\n| `org:admin` | Full access — all System Permissions, can manage org + memberships |\n| `org:member` | Read members + Read billing Permissions only |\n\nYou 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).\n\n## Billing Checks\n\n`has()` also supports plan and feature checks when Clerk Billing is enabled:\n\n```typescript\nconst { has } = await auth()\n\nhas({ plan: 'gold' })        // subscription plan\nhas({ feature: 'widgets' })  // feature entitlement\n```\n\n> **Core 2 ONLY (skip if current SDK):** `has()` only supports `role` and `permission`. Billing checks aren't available.\n\nSee `clerk-billing` for the full Billing surface and seat-limit plan model.\n\n## Enterprise SSO\n\nPer-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.\n\n```typescript\n// Strategy name for Enterprise SSO (Core 3)\nstrategy: 'enterprise_sso'\n```\n\n> **Core 2 ONLY (skip if current SDK):** Uses `strategy: 'saml'` and `user.samlAccounts` instead of `user.enterpriseAccounts`.\n\n## Gotchas\n\n### `maxAllowedMemberships` caps seats\n\n```typescript\nconst clerk = await clerkClient()\nawait clerk.organizations.createOrganization({\n  name: 'Acme Corp',\n  createdBy: userId,\n  maxAllowedMemberships: 10,\n})\n\n// Update later:\nawait clerk.organizations.updateOrganization(orgId, {\n  maxAllowedMemberships: 25,\n})\n```\n\nFor tier-based seat limits tied to a subscription, use a seat-limited Billing Plan (see `clerk-billing`).\n\n### Billing gates Permissions at the Feature level\n\nWhen 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.\n\n### Metadata updates REPLACE, not merge\n\n`updateOrganization({ publicMetadata })` overwrites all public metadata. Read first, spread, then write:\n\n```typescript\nconst org = await clerk.organizations.getOrganization({ organizationId: orgId })\nawait clerk.organizations.updateOrganization(orgId, {\n  publicMetadata: { ...org.publicMetadata, newField: 'value' },\n})\n```\n\nApplies identically to `privateMetadata` and to user metadata via `clerkClient.users.updateUser`.\n\n## Error Signatures (diagnose fast)\n\nMost \"org-related\" failures are configuration, not code. Do not edit components before checking these:\n\n| Error / symptom | Root cause | Fix |\n|---|---|---|\n| `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 />` |\n| `has({ permission: 'org:manage_members' })` always `false` | Using an invented permission slug | Use `org:sys_memberships:manage` (see roles-permissions.md catalog) |\n| `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()` |\n| `has({ permission })` `false` even with the role assigned | Feature not attached to active Plan (Billing gates permissions) | Dashboard → Billing → Plans → attach Feature |\n| `<OrganizationSwitcher />` doesn't show \"Personal Account\" | `Membership required` mode is on (the default since Aug 22, 2025) | Dashboard → Organizations settings → `Membership optional` |\n| `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 |\n| `enterpriseAccounts[0].provider` is `undefined` | Accessing `provider` at the wrong nesting level | Use `user.enterpriseAccounts[0].enterpriseConnection?.provider` |\n\n## Authorization Pattern (Complete Example)\n\nServer component protecting a slug-scoped admin page:\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\nimport { redirect } from 'next/navigation'\n\nexport default async function AdminPage({ params }: { params: { slug: string } }) {\n  const { orgSlug, has } = await auth()\n\n  if (orgSlug !== params.slug) redirect('/dashboard')\n  if (!has({ role: 'org:admin' })) redirect(`/orgs/${orgSlug}`)\n\n  return <div>Admin settings for {orgSlug}</div>\n}\n```\n\nFor middleware-level protection (Next.js) see `references/nextjs-patterns.md`.\n\n## Invitations (short form)\n\nSend from a server action or route handler:\n\n```typescript\nimport { clerkClient, auth } from '@clerk/nextjs/server'\n\nexport async function inviteMember(organizationId: string, emailAddress: string, role: string) {\n  const { userId, has } = await auth()\n\n  if (!userId) throw new Error('Not signed in')\n  if (!has({ permission: 'org:sys_memberships:manage' })) {\n    throw new Error('Not authorized to invite members')\n  }\n\n  const clerk = await clerkClient()\n  return clerk.organizations.createOrganizationInvitation({\n    organizationId,\n    inviterUserId: userId,       // required per Backend API\n    emailAddress,\n    role,                        // e.g. 'org:admin' or 'org:member'\n    redirectUrl: 'https://yourapp.com/accept-invite',\n  })\n}\n```\n\nThe full lifecycle (list, revoke, bulk create, built-in `<OrganizationProfile />` UI) lives in `references/invitations.md`.\n\n## Workflow\n\n1. **Enable** — Organizations + Membership mode in Dashboard\n2. **Create org** — via UI component or Backend API\n3. **Invite members** — Backend API or built-in UI, with `inviterUserId`\n4. **Gate access** — `has({ role })` / `has({ permission })` with canonical `org:sys_*` names\n5. **Scope routes** — `orgSlug === params.slug` on every protected page\n6. **Switch orgs** — `<OrganizationSwitcher />` handles the whole flow\n\n## See Also\n\n- `clerk-setup` — Initial Clerk install\n- `clerk-billing` — Seat-limit plans, per-plan billing, `has({ plan })` / `has({ feature })`\n- `clerk-webhooks` — Sync org events to your database (`organization.created`, `organizationMembership.*`)\n- `clerk-backend-api` — Full Backend API reference\n- `clerk-nextjs-patterns` — Framework-specific middleware, server actions, caching\n"
}

SHA-256 of public snapshot: 288a10ec57395b62459697ae1f77cac30eef3806f4ca9168a899cebe772e007b