All skills
clerk avatar

/clerk-orgs

@47c55cd official
by clerkclerk/skills83 stars
5

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.

Use this Skill: https://skilld.dev/gh/clerk/skills/clerk-orgs

This session only. Nothing lands on disk.

referencesinvitations.md

≈1.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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/server const clerk = await clerkClient() const { userId, has } = await auth()
@clerk/backend (agnostic) const clerk = createClerkClient({ secretKey }) n/a (verify the session token yourself with verifyToken imported from @clerk/backend)
@clerk/astro/server const clerk = clerkClient(context) const { userId } = context.locals.auth()
@clerk/nuxt/server const clerk = clerkClient(event) const { userId } = event.context.auth()
@clerk/express const clerk = clerkClient (after clerkMiddleware()) const { userId } = getAuth(req)

Examples below use @clerk/nextjs as the default flavor.

Create Invitation

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')
  }

  const clerk = await clerkClient()
  return clerk.organizations.createOrganizationInvitation({
    organizationId,
    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: 'alice@acme.com', role: 'org:admin' },
  { inviterUserId: userId, emailAddress: 'bob@acme.com', 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:

import { OrganizationProfile } from '@clerk/nextjs'

export default function OrgSettings() {
  return <OrganizationProfile />
}

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

  1. User clicks link → lands on your /accept-invite page with ?__clerk_ticket=... query param
  2. Your page calls signIn.create({ strategy: 'ticket', ticket }) OR signUp.create({ strategy: 'ticket', ticket }) depending on whether the user exists
  3. Clerk sets the active org on the session

Webhook Events

Listen for invitation lifecycle:

  • organizationInvitation.created
  • organizationInvitation.accepted
  • organizationInvitation.revoked

See clerk-webhooks skill for webhook setup + signature verification.

Key Rules

  • inviterUserId is 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. Default org:admin has this; org:member does not.
  • 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 createOrganizationInvitation is 250/hr, createOrganizationInvitationBulk is 50/hr. Batch wisely.

Source: SKILL.md on GitHub

No alerts6d5 checks · Risk SAFE
  • Gen Agent Trust Hub6d

    The skill provides legitimate guidance and tools for integrating Clerk Organizations into applications. It includes safety instructions requiring user confirmation for administrative actions and uses official vendor resources.

  • Socket6d

    No alerts

  • Snyk6d

    Risk: LOW · No issues

  • Runlayer7mo

    2 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 47c55cd. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated last week
What it can do
Network
metadata
{
  "author": "clerk",
  "version": "3.1.1"
}
All 1 allowed tools
WebFetch
Other metadata
compatibility
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.
  • Next.js
  • clerk
  • b2b
  • saas
  • organizations
  • rbac
  • multi-tenant
  • sso
  • authentication

README badge

README badge for clerk/skills/clerk-orgs

Manages multi-tenant B2B SaaS with Clerk Organizations — handles org creation, member invitations, role-based access control, verified domains, and enterprise SSO. Requires Organizations enabled in Clerk Dashboard and targets Next.js, React, and other Clerk SDK frameworks for team workspaces and org-scoped routing.

Generated from the current SKILL.md.

Does this work with frameworks other than Next.js?
Yes. The skill covers @clerk/nextjs by default, but the same feature-level APIs (has(), orgId, <OrganizationSwitcher />, <Show>) work across @clerk/react, @clerk/astro, @clerk/vue, @clerk/expo, @clerk/react-router, and @clerk/tanstack-react-start. Framework-specific patterns like middleware live in the nextjs-patterns reference.
What's the difference between Membership required and Membership optional?
Membership required (default) disables personal accounts and routes signed-in users through a choose-organization task, suitable for B2B-only apps. Membership optional keeps personal accounts available alongside org memberships, needed for B2C + B2B coexistence or apps with personal subscriptions.
How do I check if a user has a specific permission?
Use the has() function with a permission slug: has({ permission: 'org:sys_memberships:manage' }). System permissions prefix with org:sys_; custom permissions use org:<resource>:<action>. The full catalog is in references/roles-permissions.md.
Can I manage organizations and invitations programmatically?
Yes. The skill includes Backend API commands (clerk api) for org CRUD, membership management, and invitations. You can also use clerkClient().organizations.* in code. Billing/seat limits are handled by the clerk-billing skill.

Generated from the current SKILL.md. These answers refresh after source changes.