Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DX Guide: Using Organization Slugs in URLs #1664

Merged
merged 49 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
91a05e1
WIP
izaaklauer Oct 25, 2024
3d68044
wip
izaaklauer Oct 25, 2024
edf0892
Fixing up formatting
izaaklauer Oct 28, 2024
6ffcb5d
Ran `npm run format`
izaaklauer Oct 28, 2024
a1bf396
Apply suggestions from code review
izaaklauer Oct 29, 2024
a51a8e2
Adding missing code annotation
izaaklauer Oct 29, 2024
e56fb54
Better paragraph deliniation
izaaklauer Oct 29, 2024
a481920
Update docs/guides/using-org-slugs.mdx
izaaklauer Oct 29, 2024
90a4442
Combining client and server component sections
izaaklauer Oct 29, 2024
6154d63
Updating personal account links
izaaklauer Oct 29, 2024
131e37f
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Oct 30, 2024
30139c3
Apply suggestions from code review
izaaklauer Oct 31, 2024
4c4769e
Using the proper example repository in the title
izaaklauer Oct 31, 2024
06c736d
Apply suggestions from code review
izaaklauer Oct 31, 2024
f7bb4b5
Clarifying **organization** id
izaaklauer Oct 31, 2024
50e2dbc
Apply suggestions from code review
izaaklauer Oct 31, 2024
9b2d01e
More direct slug -> id tip
izaaklauer Oct 31, 2024
4400a26
Apply suggestions from code review
izaaklauer Oct 31, 2024
44b5482
Removing handshake explanation
izaaklauer Oct 31, 2024
d3b05e6
Apply suggestions from code review
izaaklauer Oct 31, 2024
88d8811
Apply suggestions from code review
izaaklauer Oct 31, 2024
d5c6100
Moving from /guides/ to /organizations/
izaaklauer Oct 31, 2024
ef0c553
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Oct 31, 2024
787772b
Running `npm run format`
izaaklauer Oct 31, 2024
c924ca3
Removing Further Reading
izaaklauer Oct 31, 2024
7da5194
Deleting big caution block
izaaklauer Oct 31, 2024
6e3000b
Revert "Deleting big caution block"
izaaklauer Oct 31, 2024
fe23e75
Deleting the *correct* big caution block.
izaaklauer Oct 31, 2024
2826a92
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Nov 1, 2024
1b5b76f
update file name
alexisintech Nov 4, 2024
5d7de1a
edit organization of doc; edit copy
alexisintech Nov 4, 2024
2906661
fix links
alexisintech Nov 4, 2024
d8376f2
Apply suggestions from code review
alexisintech Nov 5, 2024
11be60c
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Nov 5, 2024
c69d69d
Update docs/organizations/org-slugs-in-urls.mdx
alexisintech Nov 7, 2024
5d07c2e
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Nov 8, 2024
e16822a
Merge branch 'main' into izaak/orgs-132-dx-guide
izaaklauer Dec 18, 2024
e438200
Removing title quotes
izaaklauer Dec 18, 2024
0425c13
Addressing https://github.com/clerk/clerk-docs/pull/1664#discussion_r…
izaaklauer Dec 18, 2024
4832100
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Dec 19, 2024
d9bf42f
Updating title to make it clear it's handling failed activation.
izaaklauer Dec 19, 2024
abf0670
Clarifying and fixing formatting of a comment
izaaklauer Dec 19, 2024
7790bdb
Clarifying what's being displayed
izaaklauer Dec 19, 2024
1fafbe2
Updating to new async server function pattern
izaaklauer Dec 19, 2024
f65327b
Better copy
izaaklauer Dec 19, 2024
3957f81
update
victoriaxyz Dec 19, 2024
819716f
Merge branch 'main' into izaak/orgs-132-dx-guide
victoriaxyz Dec 19, 2024
ba9a40b
fix steps for tutorialhero
victoriaxyz Dec 19, 2024
42f5802
Apply suggestions from code review
alexisintech Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,10 @@
{
"title": "Organization workspaces",
"href": "/docs/organizations/organization-workspaces"
},
{
"title": "Use organization slugs in URLs",
"href": "/docs/organizations/org-slugs-in-urls"
}
]
]
Expand Down
276 changes: 276 additions & 0 deletions docs/organizations/org-slugs-in-urls.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
---
title: 'Use organization slugs in URLs'
izaaklauer marked this conversation as resolved.
Show resolved Hide resolved
description: Learn how to use organization slugs in URLs to manage the active Clerk organization.
---

<TutorialHero
beforeYouStart={[
{
title: "Set up a Next.js + Clerk app",
link: "/docs/quickstarts/nextjs",
icon: "nextjs",
},
{
title: "Enable organizations for your instance",
link: "/docs/organizations/overview",
icon: "globe",
}
]}
exampleRepo={[
{
title: "Sync org with URL demo",
link: "https://github.com/clerk/orgs/tree/main/examples/sync-org-with-url"
}
]}
>
- Allow users to choose an organization slug
- Include an organization slug in your URLs
- Render organization-specific content
izaaklauer marked this conversation as resolved.
Show resolved Hide resolved
</TutorialHero>

A common practice for organization-scoped spaces in an app is to prefix URLs with an organization slug. For example, imagine a B2B application named "Petstore" that has two customers: "Acmecorp" and "Widgetco." The app might use URLs such as the following:

`https://petstore.example.com/orgs/`<b>`acmecorp`</b>`/dashboard` indicates **Acmecorp**'s dashboard
`https://petstore.example.com/orgs/`<b>`widgetco`</b>`/dashboard` indicates **Widgetco**'s dashboard

You can also use an [org ID](/docs/references/javascript/organization/organization#properties) in URLs to indicate the active org:

`https://petstore.example.com/orgs/`<b>`org_1a2b3c4d5e6f7g8e`</b>`/dashboard` **Acmecorp's org ID** indicates Acmecorp's dashboard

In this guide, you'll learn how to include an org slug in your app's URLs and render information about the user's active org at runtime.

This guide is intended for apps that require org slugs in URLs. **It's recommended against doing this unless necessary**. Consider the following points before implementing:

- User relevance: Will org-specific links add value for your users? If most users belong to a single org (representing their company), they may not need slugs to identify the org when sharing links with coworkers. Public documentation, marketing, and third-party blogs are also easier to write if links aren't tied to any specific org.
- Application complexity: Adding an org slug to URLs introduces an extra piece of state to manage, increasing complexity compared to handling org context within the Clerk session alone.

<Steps>
### Define URL patterns

Define which sections of your app are scoped to orgs and which belong to [personal accounts](/docs/organizations/organization-workspaces#organization-workspaces-in-the-clerk-dashboard:~:text=Personal%20account). The following example assumes that the `/orgs/` prefix indicates an active org, followed by the org slug, and that the `/me/` prefix indicates an active personal account.

| URL | What should be active? | What should be displayed? |
| - | - | - |
| `/orgs/acmecorp` | Organization Acmecorp | Acmecorp's home |
| `/orgs/acmecorp/settings` | Organization Acmecorp | Acmecorp's settings |
| `/me` | Personal account | Personal home |
| `/me/settings` | Personal account | Personal settings |

### Configure Clerk components

The [`<OrganizationSwitcher />`](/docs/components/organization/organization-switcher) and [`<OrganizationList />`](/docs/components/organization/organization-list) components provide a robust set of options for managing slugs and IDs.

Configure the components as follows:

1. Set `hideSlug` to `false` to allow users to customize their org's URL slug when they initially create their org.
1. Set `afterCreateOrganizationUrl` and `afterSelectOrganizationUrl` to `/orgs/:slug` to navigate the user to the org's slug after they create or select an org, respectively.
1. Set `hidePersonal` to `false` to ensure the personal account is selectable.
1. Set `afterSelectPersonalUrl` to `/me` to navigate the user to their personal account after they select their personal account.

For example, say the slug of the org is `acmecorp`. After the user uses the `<OrganizationSwitcher />` or `<OrganizationList />` component to create or select an org, they'll be redirected to `/orgs/acmecorp`. After the user uses one of these components to select their personal account, they'll be redirected to `/me`.

<Tabs items={["<OrganizationSwitcher />", "<OrganizationList />"]}>
<Tab>
```tsx {{ filename: 'app/header.tsx' }}
import { OrganizationSwitcher } from '@clerk/nextjs'

export default function Header() {
return (
<OrganizationSwitcher
// prettier-ignore
hidePersonal={false}
hideSlug={false}
afterCreateOrganizationUrl="/orgs/:slug"
afterSelectOrganizationUrl="/orgs/:slug"
afterSelectPersonalUrl="/me"
/>
)
}
```
</Tab>

<Tab>
```tsx {{ filename: 'app/organization-list/[[...organization-list]]/page.tsx' }}
import { OrganizationList } from '@clerk/nextjs'

export default function OrganizationListPage() {
return (
<OrganizationList
// prettier-ignore
hidePersonal={false}
hideSlug={false}
afterCreateOrganizationUrl="/orgs/:slug"
afterSelectOrganizationUrl="/orgs/:slug"
afterSelectPersonalUrl="/me"
/>
)
}
```
</Tab>
</Tabs>

### Use `clerkMiddleware()` to set the active organization
alexisintech marked this conversation as resolved.
Show resolved Hide resolved

> [!TIP]
> If your app doesn't use `clerkMiddleware()`, or you prefer to manually set the active org, the active org can be controlled via the client-side [`setActive()`](/docs/references/javascript/clerk/session-methods) method. Refer to [this guide](/docs/guides/force-organizations#set-an-active-organization-based-on-the-url) to learn how to manually activate a specific org based on the URL.

With [`clerkMiddleware()`](/docs/references/nextjs/clerk-middleware), you can use the [`organizationSyncOptions`](/docs/references/nextjs/clerk-middleware#organization-sync-options) property to declare URL patterns that determine whether a specific org or the user's personal account should be activated.

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

In the following example, two `organizationPatterns` are defined: one for the root (e.g., `/orgs/acmecorp`) and one using 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 org indicated by the slug as the active one.

The same approach is used with `personalAccountPatterns` to match the user's personal account.

> [!WARNING]
> If no org with the specified slug exists, or the user isn't a member of the org, then `clerkMiddleware()` **won't** modify the active org, leaving the previously active one on the Clerk session.

```tsx {{ filename: 'middleware.ts', mark: [[7, 11]] }}
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware(
(auth, req) => {
// Add your middleware checks
},
{
organizationSyncOptions: {
organizationPatterns: ['/orgs/:slug', '/orgs/:slug/(.*)'],
personalAccountPatterns: ['/me', '/me/(.*)'],
},
},
)

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)(.*)',
],
}
```

### Build a component

Now that `clerkMiddleware()` is configured to activate orgs, you can build an org-specific page.

First, handle cases where the middleware can't activate the org based on the URL. This occurs if no org with the specified slug exists, or if the given user isn't a member of the org. When this happens, the middleware won't change the active org, 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 is 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 org **isn't** active. In the following example, the org slug is detected as a NextJS [Dynamic Route](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes) param, and compared to the active org slug. If they don't match, an error message is rendered and the [`<OrganizationList />`](/docs/components/organization/organization-list) component allows the user to select a valid org.

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

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

// Verify mismatch between route param and active organization slug from user's session
if (params.slug != orgSlug) {
return (
<>
<p>Sorry, organization {params.slug} is not valid.</p>
<OrganizationList
hidePersonal={false}
hideSlug={false}
afterCreateOrganizationUrl="/orgs/:slug"
afterSelectOrganizationUrl="/orgs/:slug"
afterSelectPersonalUrl="/me"
/>
</>
)
}

return (
<>
<h2>Welcome to organization {orgSlug}</h2>
</>
)
}
```

### Render org-specific content

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

<Tabs items={["Server-side","Client-side"]}>
<Tab>
To get org information on the server-side, access the [`Auth`](/docs/references/nextjs/auth-object) object. In Next.js apps, this object is returned by [`auth()`](/docs/references/nextjs/auth). In other frameworks, you can access `Auth` via the [`getAuth()`](/docs/references/nextjs/get-auth) helper.

To access other org information, such as the org name, [customize the Clerk session token](/docs/backend-requests/making/custom-session-token) to include it:

1. In the Clerk Dashboard, navigate to the [**Sessions**](https://dashboard.clerk.com/last-active?path=sessions) page.
1. In the **Customize session token** section, select **Edit**.
1. In the modal that opens, add any claim you need to your session token. For this guide, add the following:
```json
{
"org_name": "{{org.name}}"
}
```
1. Select **Save**.

Then you can access the [`sessionClaims`](/docs/references/nextjs/auth-object#:~:text=sessionClaims)
on the `Auth` object.

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

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

if (params.slug != orgSlug) {
return (
<>
<p>Sorry, organization {params.slug} is not valid.</p>
<OrganizationList
hidePersonal={false}
hideSlug={false}
afterCreateOrganizationUrl="/orgs/:slug"
afterSelectOrganizationUrl="/orgs/:slug"
afterSelectPersonalUrl="/me"
/>
</>
)
}
let orgName = authObject.sessionClaims['org_name'] as string

return <div>{orgName && <h2>Welcome to organization {orgName}</h2>}</div>
}
```
</Tab>

<Tab>
To get org information on the client-side, use the [`useOrganization()`](/docs/references/react/use-organization) hook to access the [`organization`](/docs/references/javascript/organization/organization) object.

```tsx {{ filename: 'src/app/orgs/[slug]/page.tsx' }}
'use client'

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

export default function Home({ params }: { params: { slug: string } }) {
const { organization } = useOrganization()

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"
/>
</>
)
}

return <div>{organization && <h2>Welcome to organization {organization.name}</h2>}</div>
}
```
</Tab>
</Tabs>
</Steps>
Loading