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.

referencesnextjs-patterns.md

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

Next.js Patterns for Organizations

Org-specific adaptations for @clerk/nextjs. For generic Next.js patterns (middleware strategies, auth() server vs client, 401/403 responses, server action shape, caching) see the clerk-nextjs-patterns skill.

For other frameworks see clerk-react-patterns, clerk-astro-patterns, clerk-react-router-patterns, clerk-tanstack-patterns.

Middleware: Role + Permission Protection

auth.protect() accepts the same shape as has() — pass { role }, { permission }, or a callback — so middleware can enforce org authorization without any new API:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isOrgAdminRoute = createRouteMatcher(['/orgs/:slug/admin(.*)'])
const isBillingRoute = createRouteMatcher(['/orgs/:slug/billing(.*)'])

export default clerkMiddleware(async (auth, req) => {
  if (isOrgAdminRoute(req)) {
    await auth.protect({ role: 'org:admin' })
  }
  if (isBillingRoute(req)) {
    await auth.protect({ permission: 'org:sys_billing:manage' })
  }
})

Matcher config is the standard one from clerk-nextjs-patterns — nothing org-specific about it.

URL Slug Safety Invariant

createRouteMatcher(['/orgs/:slug/(.*)']) doesn't validate that the URL slug matches the active org. A user with active org acme can hit /orgs/other-org/... and your data layer will happily reply with acme's data. Always verify on each org-scoped page:

import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function OrgPage({ params }: { params: { slug: string } }) {
  const { orgSlug, has } = await auth()
  if (orgSlug !== params.slug) redirect('/dashboard')
  if (!has({ role: 'org:admin' })) redirect(`/orgs/${orgSlug}`)
  return <AdminContent />
}

The same check applies to API routes and server actions — see below.

Server Actions: Scope Writes by orgId

'use server'
import { auth } from '@clerk/nextjs/server'

export async function createProject(name: string) {
  const { orgId, userId, has } = await auth()

  if (!userId) throw new Error('Not signed in')
  if (!orgId) throw new Error('No active organization')
  if (!has({ permission: 'org:projects:create' })) {
    throw new Error('Not authorized')
  }

  // Pull orgId from the session, never from client input — prevents cross-org writes
  return db.projects.create({ data: { name, orgId, createdBy: userId } })
}

Rule: always bind orgId from auth() at the database layer. Never trust a client-supplied org identifier.

API Route Example

// app/api/orgs/[slug]/members/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ slug: string }> },
) {
  const { orgSlug, orgId, has } = await auth()
  const { slug } = await params

  if (orgSlug !== slug) {
    return NextResponse.json({ error: 'wrong org' }, { status: 403 })
  }
  if (!has({ permission: 'org:sys_memberships:read' })) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 })
  }

  const clerk = await clerkClient()
  const { data } = await clerk.organizations.getOrganizationMembershipList({
    organizationId: orgId!,
  })

  return NextResponse.json({ members: data })
}

(Generic 401 vs 403 response policy lives in clerk-nextjs-patterns/references/api-routes.md.)

Key Rules

  • Validate orgSlug === params.slug on every org-scoped surface. The slug in the URL is an identifier; the active org in the session is the authority. Don't let them diverge.
  • Bind orgId from auth() at the database layer. Never let a client supply it.
  • Use auth.protect({ role / permission }) in middleware for fast-path enforcement; rely on page-level checks for defense in depth.
  • redirect() throws — it doesn't return. Don't put code after it expecting to run.

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.