Use Organizations
Learn how to use organizations in your application.
In the starter kit, the active organization is stored in the Better Auth session. The active organization ID is available in session.activeOrganizationId and can be accessed using Better Auth's hooks and APIs.
How Active Organization Works
The active organization is managed by Better Auth and stored in the user's session. When a user switches organizations, the activeOrganizationId in the session is updated, and this organization becomes available throughout your application.
Client-Side Usage
Using Better Auth's Hook
Use authClient.useActiveOrganization() to access the active organization:
'use client';
import { authClient } from '@/lib/auth/client';
export function OrganizationContent() {
const { data: activeOrganization, isPending } =
authClient.useActiveOrganization();
if (isPending) {
return <div>Loading...</div>;
}
if (!activeOrganization) {
return <div>No active organization found</div>;
}
return (
<div>
<h1>{activeOrganization.name}</h1>
</div>
);
}Switching Organizations
Switch organizations using authClient.organization.setActive():
'use client';
import { useRouter } from 'next/navigation';
import { authClient } from '@/lib/auth/client';
export function OrganizationSwitcher() {
const router = useRouter();
const handleSwitch = async (organizationId: string) => {
// Set the active organization in Better Auth session
await authClient.organization.setActive({
organizationId
});
// Navigate to the organization dashboard
router.push('/dashboard/organization');
};
return (
<select onChange={(e) => handleSwitch(e.target.value)}>
{/* Organization options */}
</select>
);
}Getting Active Organization from Session
You can also access the active organization ID directly from the session:
'use client';
import { useSession } from '@/hooks/use-session';
export function Example() {
const { session } = useSession();
const activeOrganizationId = session?.activeOrganizationId;
return <div>Active Org ID: {activeOrganizationId}</div>;
}Server-Side Usage
Get Active Organization from Session
Get the active organization from the session:
import { getOrganizationById, getSession } from '@/lib/auth/server';
export default async function OrganizationPage() {
const session = await getSession();
if (!session?.session.activeOrganizationId) {
return <div>No active organization</div>;
}
const organization = await getOrganizationById(
session.session.activeOrganizationId
);
if (!organization) {
return <div>Organization not found</div>;
}
return <div>Active organization: {organization.name}</div>;
}Get Organization by ID
Get organization data for a specific organization ID:
import { getOrganizationById } from '@/lib/auth/server';
export async function getOrganization(organizationId: string) {
const organization = await getOrganizationById(organizationId);
return organization;
}Using in tRPC
The active organization is automatically available in protectedOrganizationProcedure:
import { createTRPCRouter, protectedOrganizationProcedure } from '@/trpc/init';
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
export const organizationRouter = createTRPCRouter({
get: protectedOrganizationProcedure.query(async ({ ctx }) => {
// ctx.organization is guaranteed to exist
// ctx.membership contains the user's role
return {
organization: ctx.organization,
role: ctx.membership.role
};
}),
update: protectedOrganizationProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ input, ctx }) => {
// Only allow admins/owners to update
if (ctx.membership.role !== 'admin' && ctx.membership.role !== 'owner') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Only admins can update organizations'
});
}
// Update organization using Better Auth API
await authClient.organization.update({
organizationId: ctx.organization.id,
name: input.name
});
return { success: true };
})
});Organization Switching
Client-Side Switching
Switch organizations using Better Auth's API:
'use client';
import { useRouter } from 'next/navigation';
import { authClient } from '@/lib/auth/client';
export function OrganizationSwitcher() {
const router = useRouter();
const handleSwitch = async (organizationId: string) => {
try {
// Set the active organization in Better Auth session
await authClient.organization.setActive({
organizationId
});
// Navigate to the organization dashboard
router.push('/dashboard/organization');
} catch (error) {
console.error('Failed to switch organization:', error);
}
};
return (
<select onChange={(e) => handleSwitch(e.target.value)}>
{/* Organization options */}
</select>
);
}Server-Side Switching
Update the active organization in the session:
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { assertUserIsOrgMember, getSession } from '@/lib/auth/server';
export async function POST(request: Request) {
const { organizationId } = await request.json();
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Verify user is member of organization
await assertUserIsOrgMember(organizationId, session.user.id);
// Update active organization in session
await auth.api.setActiveOrganization({
headers: await headers(),
body: {
organizationId
}
});
return NextResponse.json({ success: true });
}Listing User's Organizations
Get all organizations a user is a member of:
import { createTRPCRouter, protectedProcedure } from '@/trpc/init';
import { asc, eq, getTableColumns } from 'drizzle-orm';
import { db } from '@/lib/db';
import { memberTable, organizationTable } from '@/lib/db/schema';
export const organizationRouter = createTRPCRouter({
list: protectedProcedure.query(async ({ ctx }) => {
const organizations = await db
.select({
...getTableColumns(organizationTable),
membersCount: db
.$count(
memberTable,
eq(memberTable.organizationId, organizationTable.id)
)
.as('membersCount')
})
.from(organizationTable)
.innerJoin(
memberTable,
eq(organizationTable.id, memberTable.organizationId)
)
.where(eq(memberTable.userId, ctx.user.id))
.orderBy(asc(organizationTable.createdAt));
return organizations.map((org) => ({
...org,
slug: org.slug || ''
}));
})
});Client-side:
'use client';
import { trpc } from '@/trpc/client';
export function OrganizationsList() {
const { data: organizations, isLoading } = trpc.organization.list.useQuery();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{organizations?.map((org) => (
<div key={org.id}>{org.name}</div>
))}
</div>
);
}Best Practices
- Always check membership - Verify user is a member before allowing access
- Use protectedOrganizationProcedure - Automatically handles organization scoping
- Handle loading states - Show loading indicators while fetching organization
- Handle missing organizations - Provide fallback UI when no organization is active
- Validate permissions - Check roles before allowing actions
- Use session-based approach - The active organization is stored in the session, not the URL