Session Handling

Protect routes and access session data in server and client components.

Internal pages (/admin/*) are protected at the proxy/middleware level. This means authentication is checked before any page code executes - but for other routes, please always authenticate users using the getSession() function.

Proxy-Level Protection

The proxy (apps/web/proxy.ts) automatically guards internal routes:

  • /admin/* - Require admin role
  • /auth/* - Redirects authenticated users to dashboard

For all other routes, always check for authentication using the getSession() function.

Server Component

import { getSession } from '@kit/better-auth/context';
export default async function MyPage() {
const session = await getSession();
if (!session) {
// Handle unauthenticated state
return null;
}
}

Server-Side Session Access

Use getSession() to access the current session in server components or server actions:

Server Component

import { getSession } from '@kit/better-auth/context';
export default async function MyPage() {
const session = await getSession();
if (!session) {
// Handle unauthenticated state
return null;
}
return <div>Hello, {session.user.name}</div>;
}

The function is cached per request via React's cache(), so multiple calls within the same request are efficient.

Session Data Structure

interface Session {
user: {
id: string;
name: string;
email: string;
image: string | null;
emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
role: string | null;
};
session: {
id: string;
userId: string;
expiresAt: Date;
activeOrganizationId: string | null;
};
}

Account Context

For protected pages that need user context with automatic redirects, use getAccountContext():

apps/web/app/[locale]/(internal)/dashboard/page.tsx

import { getAccountContext } from '@kit/better-auth/context';
export default async function DashboardPage() {
// Redirects to sign-in if unauthenticated
const context = await getAccountContext();
if (context.isOrganization) {
return <OrgDashboard orgId={context.activeOrganizationId} />;
}
return <PersonalDashboard user={context.user} />;
}

The getAccountContext function returns the following structure:

Account Context Structure

interface ServerAccountContext {
isPersonal: boolean;
isOrganization: boolean;
activeOrganizationId: string | null;
user: {
id: string;
name: string;
email: string;
image: string | null;
createdAt: Date;
updatedAt: Date;
emailVerified: boolean;
};
}

Require Organization Context

For pages that require an active organization:

Organization-only page

import { requireActiveOrganizationId } from '@kit/better-auth/context';
export default async function MembersPage() {
// Redirects to /dashboard if not in org context
const orgId = await requireActiveOrganizationId();
const members = await loadMembers(orgId);
return <MembersList members={members} />;
}

Admin Protection

Require Admin in Pages

Use requireAdmin() in admin-only pages or layouts:

apps/web/app/[locale]/(internal)/admin/layout.tsx

import { requireAdmin } from '@kit/auth/require-admin';
export default async function AdminLayout({ children }) {
const admin = await requireAdmin();
return <AdminLayoutWrapper user={admin}>{children}</AdminLayoutWrapper>;
}

While this is already checked at the proxy level, we re-run the check in the server component to ensure the admin status is still valid and abundance of caution. We recommend doing this in all admin pages, layouts and server actions to ensure the admin status is always valid.

Check Admin Status

Use isUserAdmin() when you need to check admin status without redirecting:

Conditional rendering

import { isUserAdmin } from '@kit/auth/require-admin';
export default async function Header() {
const isAdmin = await isUserAdmin();
return (
<nav>
<Link href="/dashboard">Dashboard</Link>
{isAdmin && <Link href="/admin">Admin</Link>}
</nav>
);
}

Protected Server Actions

Use authenticatedActionClient to protect server actions:

apps/web/app/[locale]/(internal)/settings/_lib/actions.ts

'use server';
import { authenticatedActionClient } from '@kit/action-middleware';
import { z } from 'zod';
const updateProfileSchema = z.object({
name: z.string().min(1),
});
export const updateProfileAction = authenticatedActionClient
.inputSchema(updateProfileSchema)
.action(async ({ parsedInput, ctx }) => {
// ctx.user is guaranteed to exist
const { user } = ctx;
await updateUser(user.id, parsedInput);
return { success: true };
});

The middleware throws an "Unauthorized" error if the session is invalid.

Client-Side Session

Use authClient.useSession() in client components:

Client component

'use client';
import { authClient } from '@kit/better-auth/client';
export function UserMenu() {
const session = authClient.useSession();
if (session.isPending) {
return <Skeleton />;
}
if (!session.data) {
return <SignInButton />;
}
return <Avatar name={session.data.user.name} />;
}

Session Hook Return Type

{
data: Session | null;
isPending: boolean;
error: Error | null;
}

Summary

Below is a summary of the functions and their use cases:

PatternUse CaseLocation
getSession()Get session dataServer components, server actions
getAccountContext()Get user context with redirectProtected pages
requireActiveOrganizationId()Require org contextOrg-only pages
requireAdmin()Require admin roleAdmin pages/layouts
isUserAdmin()Check admin without redirectConditional rendering
authenticatedActionClientProtect server actionsServer actions
authClient.useSession()Client-side sessionClient components

Next: Personal Accounts →