Organizations in the URL
Serve each organization under its own /[org] path with requireTenantSession, and send visitors from other organizations to the right sign-in.
Multi-tenant applications often put the organization in the path: /acme/projects, /globex/settings. The
path segment is the organization's alias (its slug), and every page below it should run in that organization's
. Two things can go wrong without help: an unknown or deactivated alias renders a broken page instead of a
404, and a person signed in to Acme who follows a link to /globex sees Globex pages rendered around an Acme
session. iamNext.requireTenantSession({ slug }) handles both: it resolves the alias and requires a session in
that tenant, sending everyone else to sign in to the right organization.
The organization layout
Put it in app/[org]/layout.tsx, and every page below gets the resolved tenant.
import type { ReactNode } from 'react';
import Link from 'next/link';
import { iamNext } from '@/lib/iam-next';
import { signOut } from '../auth-actions';
export default async function OrgLayout(props: {
params: Promise<{ org: string }>;
children: ReactNode;
}) {
const { org } = await props.params;
// Unknown aliases 404; visitors signed in elsewhere go to /login?org=...&next=...
const { tenant, session } = await iamNext.requireTenantSession({ slug: org });
return (
<>
<header>
<Link href={`/${org}`}>
<strong>{tenant.name}</strong>
</Link>
<form action={signOut}>
<span>{session.identity.name} </span>
<button type="submit">Sign out</button>
</form>
</header>
{props.children}
</>
);
}requireTenantSession({ slug, headers?, returnTo?, redirectTo? }) returns { tenant, session }, where tenant
is { tenantId, name, type, slug }.
- Resolution. The alias goes through
tenants.lookup, which finds active tenants with active ancestors only. Unknown or inactive aliases callnotFound(). - Wrong organization. A visitor who is signed out, or signed in to a different organization, is redirected to
/login?org={slug}&next=...(orredirectTo). The login page can pre-select the organization or offer the account switcher. - Return path.
?next=isreturnTo, or the path the middleware forwarded, so a deep link survives sign-in.
To resolve an alias without requiring a session, for example on a public landing page, use
iamNext.tenant(slug). It returns the same summary, or null when the alias does not exist or is inactive.
Pages below the layout
Wrap each page with iamNext.page as well: Next.js keeps layouts across client-side navigations instead of
re-rendering them, and the page needs the session anyway. Below the layout, the session's tenant is the
organization's tenant, which is also the default tenant for authorize.
import { iamNext } from '@/lib/iam-next';
export default iamNext.page(async (_props: { params: Promise<{ org: string }> }, { session }) => {
const tenantId = session.session.tenantId;
const document = { type: 'document', id: 'roadmap' };
const access = await iamNext.can({
tenantId,
checks: [
{ action: 'documents:read', resource: document },
{ action: 'documents:write', resource: document },
{ action: 'iam:identities:read' },
],
});
return (
<main>
<h1>Welcome, {session.identity.name}</h1>
<iamNext.Can action="documents:write" resource={document} fallback={<p>Read-only.</p>}>
<p>You may edit the roadmap.</p>
</iamNext.Can>
<pre>{JSON.stringify(access, null, 2)}</pre>
</main>
);
});The layout's requireTenantSession and the page's page() share one session lookup, because the no-argument
session read is memoized per request.
The login page
The redirect carries ?org= and ?next=. Pass both to the sign-in form: org fills (or replaces) the
organization field, and next is where a completed sign-in returns.
import { SignInForm } from '@better-iam/next/client';
import { signIn } from '../auth-actions';
export default async function Login(props: { searchParams: Promise<{ next?: string; org?: string }> }) {
const { next, org } = await props.searchParams;
return <SignInForm action={signIn} next={next} org={org} keepSignedIn passwordless />;
}Other ways to find the organization at sign-in:
- Email domain.
iamNext.authActions({ discover: true })finds the tenant from the verified domain of the email (domains.discover) when the form carries notenantIdororg. See Enterprise onboarding for verified domains. - Your own rule.
authActions({ resolveTenant: async (form) => ... })picks the tenant from the submission. - Account switcher. A person with linked in several organizations can list them with
links.listand switch withlinks.switch. Identities in different tenants stay separate, even when linked; see Tenants and identities.
Next steps
Better IAM is created by Sean Filimon
Last updated
Server actions and forms
Call Better IAM from Next.js server actions with the in-process client, guard mutations with action(), and drop in sign-in, step-up, and reset forms.
Middleware
Redirect signed-out visitors at the edge with createIamMiddleware, keep public paths open, and forward the requested path so ?next= fills itself.