Hide Personal Accounts and force organizations
You will learn how to:
- Hide Personal Accounts from UI components
- Detect an active organization
- Set an active organization
- Limit access to users with active organizations only
Introduction
In this guide, you will learn how to hide a user's Personal Account in order to appear as if they only have access to organizations. You will also learn how to limit access to your application to only users with active organizations, further enforcing organization-centric access. This is useful for applications that are built for organizations only, such as B2B applications.
This guide will be written for Next.js applications using App Router, but the same concepts can be applied to any application using Clerk.
Hide Personal Accounts from UI components
To begin forcing organizations in your application, you need to remove a user's Personal Account from the UI. A user's Personal Account cannot be disabled; it can only be hidden.
If you have an application set up to use organizations, you might have the <OrganizationList />
and <OrganizationSwitcher />
components in your application. These components will show a user's Personal Account by default, but you can hide it by passing the hidePersonal
prop.
app/organization-list/[[...organization-list]]/page.tsximport { OrganizationList } from '@clerk/nextjs' export default function OrganizationListPage() { return ( <OrganizationList hidePersonal={true} /> ); };
app/sidebar.tsximport { OrganizationSwitcher } from '@clerk/nextjs' export default function Sidebar() { return ( <OrganizationSwitcher hidePersonal={true} /> ); };
Detect and set an active organization
With Clerk, a user can have many organization memberships, but only one of them can be active at a time.
Detect an active organization
A session will always include an orgId
via the Auth
object. This can be used to detect if a user has an active organization.
The auth()
helper function can be used to get the orgId
from the session server-side. If the orgId
is null
, then the user does not have an active organization.
import { auth } from '@clerk/nextjs/server' export default function Layout() { const { orgId } = auth(); const hasActiveOrg = orgId !== null; ... }
The useAuth()
hook can be used to get the orgId
from the session client-side. If the orgId
is null
, then the user does not have an active organization.
"use client" import { useAuth } from '@clerk/nextjs' export default function Layout() { const { orgId } = useAuth(); const hasActiveOrg = orgId !== null; ... }
Set an active organization
In the case of Clerk components, an organization will automatically be set as active each time the user creates an organization, accepts an invitation, or selects a membership from the organization switcher. In the case of custom flows, you will need to implement the logic for setting an organization as active. Clerk's useOrganizationList()
hook provides a setActive
method to help you with this.
In the example below, a custom organization switcher is created. It allows a user to select an organization from a list of their memberships. The useOrganizationList()
hook is used to fetch a list of the user's memberships, and the setActive
method is used to set the selected organization as active.
Setting an active organization can only be performed client-side.
app/custom-org-switcher.tsx"use client" import { useOrganizationList } from "@clerk/nextjs"; export const CustomOrgSwitcher = () => { const { isLoaded, setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true, }, }); if (!isLoaded) { return <>Loading</>; } return ( <> <ul> {userMemberships.data?.map((mem) => ( <li key={mem.id}> <span>{mem.organization.name}</span> <button onClick={() => setActive({ organization: mem.organization.id })} > Select </button> </li> ))} </ul> <button disabled={!userMemberships.hasNextPage} onClick={() => userMemberships.fetchNext()} > Load more </button> </> ); };
Set an active organization based on the URL
This approach still depends on the setActive
method, which only runs on the client. Due to this limitation, during SSR, it is possible that orgId
from auth()
returns an incorrect value that does not match the organization ID in the URL.
In some cases, you might want to set an active organization based on the URL. For example, if you have a URL like /organizations/org_123
, you might want to set org_123
as the active organization.
In the example below, a component called SyncActiveOrganization
is created. The Next.js useParams()
(opens in a new tab) hook is used to get the organization ID from the URL, and then the setActive
method is used to set the organization as active.
app/utils/sync-active-organization.tsx"use client" import { useEffect } from "react" import { redirect, useParams } from "next/navigation" import { useAuth, useOrganizationList } from "@clerk/nextjs" export function SyncActiveOrganization() { const { setActive, isLoaded } = useOrganizationList(); // Get the organization ID from the session const { orgId } = useAuth(); // Get the organization ID from the URL const { orgId: urlOrgId } = useParams() as { orgId: string }; useEffect(() => { if (!isLoaded) return; // If the org ID in the URL is not valid, redirect to the homepage if (!urlOrgId?.startsWith("org_")) { redirect("/"); return; } // If the org ID in the URL is not the same as the org ID in the session (the active organization), set the active organization to be the org ID from the URL if (urlOrgId && urlOrgId !== orgId) { void setActive({ organization: urlOrgId }); } }, [orgId, isLoaded, setActive, urlOrgId]) return null; }
Now you can place the SyncActiveOrganization
helper that you created above in a layout(opens in a new tab) and use the layout for all pages that require an active organization. In the example below, the SyncActiveOrganization
component will run on every page in the /app/[orgId]
directory.
app/[orgId]/layout.tsximport { type PropsWithChildren } from "react" import { SyncActiveOrganization } from '../utils/sync-active-organization' export default function OrganizationLayout(props: PropsWithChildren) { return ( <> <SyncActiveOrganization/> {props.children} </> ); }
Limit access to users with active organizations only
Now that you have hidden Personal Accounts from the UI and can detect and set an active organization, you can limit access to your application to users with active organizations only. This will ensure that users without active organizations cannot access your application.
It's possible for a user to be signed in, but not have an active organization. This can happen in two cases:
- The user created an account and hasn't created an organization yet
- The user joined or created multiple organizations, and left or deleted the active organization
There are two ways to limit access to users with active organizations only:
Based on your use case, you can decide to limit users either in the entire app or a specific part of it. For example, a B2B application might need to limit access to only users with an active organization, whereas a B2B2C application might limit only the /dashboard
path to users with an active organization.
Limit access using the clerkMiddleware()
helper
Clerk's clerkMiddleware()
helper can be used in both App Router and Pages Router applications to limit access to users with active organizations only.
In the example below, the clerkMiddleware()
helper is used to redirect signed in users to the organization selection page if they are not active in an organization.
middleware.tsimport { clerkMiddleware } from '@clerk/nextjs/server'; export default clerkMiddleware((auth, req) => { // Redirect signed in users to organization selection page if they are not active in an organization if(auth.userId && !auth.orgId && req.nextUrl.pathname !== "/org-selection"){ const searchParams = new URLSearchParams({redirectUrl: req.url}); const orgSelection = new URL(`/org-selection?${searchParams.toString()}`, req.url); return NextResponse.redirect(orgSelection); } }); export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };
To limit access to a specific part of the application, you can check if the path the user is visiting is the path you want to limit access to. In this example, the /dashboard
path is limited to users with active organizations only. If the user is not active in an organization, they will be redirected to the /dashboard/org-selection
page.
middleware.tsimport { clerkMiddleware } from '@clerk/nextjs/server'; export default clerkMiddleware((auth, req) => { // Redirect signed in users to organization selection page if they are not active in an organization if( auth.userId && !auth.orgId && req.nextUrl.pathname.startsWith('/dashboard') && req.nextUrl.pathname !== "/dashboard/org-selection" ){ const searchParams = new URLSearchParams({redirectUrl: req.url}) const orgSelection = new URL(`/dashboard/org-selection?${searchParams.toString()}`, req.url); return NextResponse.redirect(orgSelection); } }); export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };
Now that you have configured your middleware to redirect users to the /org-selection
page if they are not active in an organization, you need to create the /org-selection
page. This page will allow users to select an organization or create their own organization.
app/org-selection/page.tsx"use client" import { OrganizationList } from "@clerk/nextjs" import { useSearchParams } from "next/navigation"; export default function OrganizationSelection() { const searchParams = useSearchParams(); const redirectUrl = searchParams.get('redirectUrl') ?? "/"; return ( <section> <h1>Welcome to the Organization Selection page.</h1> <p> This part of the application requires the user to select an organization in order to proceed. If you are not part of an organization, you can accept an invitation or create your own organization. </p> <OrganizationList hidePersonal={true} afterCreateOrganizationUrl={redirectUrl} afterSelectOrganizationUrl={redirectUrl} /> </section> ) }
pages/org-selection.tsximport { OrganizationList } from "@clerk/nextjs" import { useRouter } from "next/router"; export default function OrganizationSelection() { const { query } = useRouter(); const redirectUrl = query.redirectUrl ?? "/" return ( <section> <h1>Welcome to the Organization Selection page.</h1> <p> This part of the application requires the user to select an organization in order to proceed. If you are not part of an organization, you can accept an invitation or create your own organization. </p> <OrganizationList hidePersonal={true} afterCreateOrganizationUrl={redirectUrl} afterSelectOrganizationUrl={redirectUrl} /> </section> ) }
Limit access using an App Router layout
In Next.js App Router applications, instead of using clerkMiddleware()
, you also have the option to use a layout to limit access to users with active organizations only.
In the example below, a component called RequiredActiveOrgLayout
is created. This component will be used as a layout for all pages that require an active organization. If the user has an active organization, the route the user is visting will be rendered. If the user does not have an active organization, the organization selection page will be rendered.
app/(with-active-organization)/layout.tsximport { OrganizationList } from "@clerk/nextjs" import { auth } from "@clerk/nextjs/server" import { PropsWithChildren } from "react" export default function RequiredActiveOrgLayout(props: PropsWithChildren) { // Get the organization ID from the session const { orgId } = auth() // If the user has an active organization, render the children if (orgId) { return props.children } // If the user does not have an active organization, render the organization selection page return ( <section> <h1>Welcome to the Organization Selection page.</h1> <p> This part of the application requires the user to select an organization in order to proceed. If you are not part of an organization, you can accept an invitation or create your own organization. </p> <OrganizationList hidePersonal={true} /> </section> ) }
Finished ๐
You have configured your Clerk application to hide personal accounts and enforce organization-centric access. By following this guide, you've learned how to:
- hide Personal Accounts from UI components
- detect an active organization by using Clerk's
auth()
helper function oruseAuth()
hook - set an active organization automatically through Clerk components or manually using the
setActive
method - set an active organization based on the URL
- limit access to your application to only users with active organizations by utilizing Clerk's
clerkMiddleware()
helper or by implementing an App Router layout
Now that you've completed these steps, your application is well-configured for organizational use, providing a streamlined experience for users associated with organizations.
Last updated on March 8, 2024