General
Organizations

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:

components/organization-content.tsx
'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():

components/organization-switcher.tsx
'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:

components/example.tsx
'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:

app/(saas)/dashboard/organization/page.tsx
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:

lib/organization/get-organization.ts
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:

trpc/routers/organization/index.ts
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:

components/organization-switcher.tsx
'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:

app/api/organization/switch/route.ts
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:

trpc/routers/organization/index.ts
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:

components/organizations-list.tsx
'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

  1. Always check membership - Verify user is a member before allowing access
  2. Use protectedOrganizationProcedure - Automatically handles organization scoping
  3. Handle loading states - Show loading indicators while fetching organization
  4. Handle missing organizations - Provide fallback UI when no organization is active
  5. Validate permissions - Check roles before allowing actions
  6. Use session-based approach - The active organization is stored in the session, not the URL