Skip to main content
Docs

Use organization slugs in URLs

Organization slugs are human-readable URL identifiers that help users reference which organization they're working in. A common pattern for organization-scoped areas in an application is to include the organization slug in the URL path.

For example, a B2B application named "Petstore" has two customer organizations: Acmecorp and Widgetco. Each organization uses its name as a slug in the URL:

  • Acmecorp: https://petstore.example.com/orgs/acmecorp/dashboard
  • Widgetco: https://petstore.example.com/orgs/widgetco/dashboard

Alternatively, can be used to identify organizations in URLs:

  • Acmecorp: https://petstore.example.com/orgs/org_1a2b3c4d5e6f7g8e/dashboard
  • Widgetco: https://petstore.example.com/orgs/org_1a2b3c4d5e6f7g8f/dashboard

When to use organization slugs

This feature is intended for apps that require organization slugs in URLs. Adding slugs to URLs isn't recommended unless necessary.

Use organization slugs if:

  • Users frequently share links for public-facing content (e.g., documentation, marketing materials, and third-party blogs)
  • Users regularly switch between multiple organizations
  • Organization-specific URLs provide meaningful context

Don't use organization slugs if:

  • Most users belong to only one organization
  • You want to keep URLs simple and consistent
  • You're primarily using the Clerk session for organization context

This guide shows you how to add organization slugs to your app's URLs, configure Clerk components to handle slug-based navigation, and access organization data based on the URL slug at runtime.

Configure <OrganizationSwitcher /> and <OrganizationList />

The <OrganizationSwitcher /> and <OrganizationList /> components provide a robust set of options to manage organization slugs and IDs in your application's URLs.

Set the following properties to configure the components to handle slug-based navigation:

  • Set hideSlug to false to allow users to customize the organization's URL slug when creating an organization.
  • Set afterCreateOrganizationUrl to /orgs/:slug to navigate the user to the organization's slug after creating an organization.
  • Set afterSelectOrganizationUrl to /orgs/:slug to navigate the user to the organization's slug after selecting it.

For example, if the organization has the slug acmecorp, when a user creates or selects that organization using either component, they'll be redirected to /orgs/acmecorp.

components/Header.tsx
import { OrganizationSwitcher } from '@clerk/nextjs'

export default function Header() {
  return (
    <OrganizationSwitcher
      hideSlug={false} // Allow users to customize the org's URL slug
      afterCreateOrganizationUrl="/orgs/:slug" // Navigate to the org's slug after creating an org
      afterSelectOrganizationUrl="/orgs/:slug" // Navigate to the org's slug after selecting  it
    />
  )
}
app/organization-list/[[...organization-list]]/page.tsx
import { OrganizationList } from '@clerk/nextjs'

export default function OrganizationListPage() {
  return (
    <OrganizationList
      hideSlug={false} // Allow users to customize the org's URL slug
      afterCreateOrganizationUrl="/orgs/:slug" // Navigate to the org's slug after creating an org
      afterSelectOrganizationUrl="/orgs/:slug" // Navigate to the org's slug after selecting it
    />
  )
}

Tip

If your app doesn't use clerkMiddleware(), or you prefer to manually set the active organization, use the method to control the active organization on the client side.

With , you can use the property to declare URL patterns that determine whether a specific organization should be activated.

If the middleware detects one of these patterns in the URL and finds that a different organization is active in the session, it'll attempt to set the specified organization as the active one.

In the following example, two organizationPatterns are defined: one for the root (e.g., /orgs/acmecorp) and one as the wildcard matcher (.*) to match /orgs/acmecorp/any/other/resource. This configuration ensures that the path /orgs/:slug with any optional trailing path segments will set the organization indicated by the slug as the active one.

Warning

If no organization with the specified slug exists, or if the user isn't a member of the organization, then clerkMiddleware() won't modify the active organization. Instead, it will leave the previously active organization unchanged on the Clerk session.

middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware(
  (auth, req) => {
    // Add your middleware checks
  },
  {
    organizationSyncOptions: {
      organizationPatterns: [
        '/orgs/:slug', // Match the org slug
        '/orgs/:slug/(.*)', // Wildcard match for optional trailing path segments
      ],
    },
  },
)

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}

Handle failed activation

Now that clerkMiddleware() is configured to activate organizations, you can build an organization-specific page while handling cases where the organization can't be activated.

Failed activation occurs if no organization with the specified slug exists, or if the given user isn't a member of the organization. When this happens, the middleware won't change the active organization, leaving the previously active one unchanged.

For troubleshooting, a message will also be logged on the server:

Clerk: Organization activation handshake loop detected. This is likely due to an invalid organization ID or slug. Skipping organization activation.

It's ultimately the responsibility of the page to ensure that it renders the appropriate content for a given URL, and to handle the case where the expected organization isn't active.

In the following example, the organization slug is detected as a Next.js Dynamic Route param and passed as a parameter to the page. If the slug doesn't match the active organization slug, an error message is rendered and the <OrganizationList /> component allows the user to select a valid organization.

app/orgs/[slug]/page.tsx
import { auth } from '@clerk/nextjs/server'
import { OrganizationList } from '@clerk/nextjs'

export default async function Home({ params }: { params: { slug: string } }) {
  const { orgSlug } = await auth()
  const { slug } = await params

  // Check if the organization slug from the URL params doesn't match
  // the active organization slug from the user's session.
  // If they don't match, show an error message and the list of valid organizations.
  if (slug != orgSlug) {
    return (
      <>
        <p>Sorry, organization {slug} is not valid.</p>
        <OrganizationList
          hideSlug={false}
          afterCreateOrganizationUrl="/orgs/:slug"
          afterSelectOrganizationUrl="/orgs/:slug"
        />
      </>
    )
  }

  return <div>Welcome to organization {orgSlug}</div>
}

Render organization-specific content

Use the following tabs to learn how to access organization information on the server-side and client-side.

To get organization information on the server-side, access the object which includes the active org's orgId and orgSlug and the current user's orgRole and orgPermissions. To access additional organization information server-side, like the organization name, you can store the additional information in the user's session token. To customize the session token, do the following:

  1. In the Clerk Dashboard, navigate to the Sessions page.

  2. Under Customize session token, in the Claims editor, add any claim you need to your session token. For this guide, add the following claim:

    {
      "org_name": "{{org.name}}"
    }
  3. Select Save.

Now that you've added the claim to the session token, you can access it from the property on the Auth object.

app/orgs/[slug]/page.tsx
import { auth } from '@clerk/nextjs/server'
import { OrganizationList } from '@clerk/nextjs'

export default async function Home({ params }: { params: { slug: string } }) {
  const { orgSlug, sessionClaims } = await auth()
  const { slug } = await params

  // Check if the organization slug from the URL params doesn't match
  // the active organization slug from the user's session.
  // If they don't match, show an error message and the list of valid organizations.
  if (slug != orgSlug) {
    return (
      <>
        <p>Sorry, organization {slug} is not valid.</p>
        <OrganizationList
          hideSlug={false}
          afterCreateOrganizationUrl="/orgs/:slug"
          afterSelectOrganizationUrl="/orgs/:slug"
        />
      </>
    )
  }

  // Access the organization name from the session claims
  let orgId = sessionClaims['org_id'] as string

  return <div>{orgId && `Welcome to organization ${orgId}`}</div>
}

To get organization information on the client-side, use the useOrganization() hook to access the object.

app/orgs/[slug]/page.tsx
'use client'

import { OrganizationList, useOrganization } from '@clerk/nextjs'

export default function Home({ params }: { params: { slug: string } }) {
  // Use `useOrganization()` to access the currently active organization's `Organization` object
  const { organization } = useOrganization()

  // Check if the organization slug from the URL params doesn't match
  // the active organization slug from the user's session.
  // If they don't match, show an error message and the list of valid organizations.
  if (!organization || organization.slug != params.slug) {
    return (
      <>
        <p>Sorry, organization {params.slug} is not valid.</p>
        <OrganizationList
          hidePersonal={false}
          hideSlug={false}
          afterCreateOrganizationUrl="/orgs/:slug"
          afterSelectOrganizationUrl="/orgs/:slug"
          afterSelectPersonalUrl="/me"
        />
      </>
    )
  }

  // Access the organization name from the `Organization` object
  return <div>{organization && `Welcome to organization ${organization.name}`}</div>
}

Feedback

What did you think of this content?

Last updated on