Session Handling

Protect routes and access session data in React Server Components and client components with Better Auth.

Protect pages with getSession() in React Server Components or useSession() in client components. Sessions are stored in PostgreSQL via Prisma and cached per request.

This page is part of the Authentication documentation.

Session handling verifies users are authenticated before showing protected content. Use getSession() in React Server Components and server actions to check authentication server-side. Use authClient.useSession() in client components for reactive session state. Admin routes (/admin/*) have additional protection at the proxy level, but always re-verify with requireAdmin() in components. The session functions are cached per request using React's cache(), so multiple calls are efficient.

Session handling is the process of verifying a user's authentication state and accessing their session data to protect routes and personalize content.

Use getSession() when: you need to check auth in React Server Components, server actions, or API routes.

Use useSession() when: you need reactive session state in client components (loading states, conditional rendering).

Use requireAdmin() when: the page or action requires super admin privileges.

Session Storage

Better Auth stores sessions in your PostgreSQL database via Prisma:

  • Session tokens are stored in HTTP-only cookies (secure, not accessible to JavaScript)
  • Session data is stored in the Session table in your database
  • Default expiration is 7 days (configurable in Better Auth setup)

This architecture allows:

  • Session invalidation across all devices
  • Server-side session revocation
  • No JWT expiration issues

Proxy-Level Protection

The proxy at apps/web/proxy.ts provides first-gate protection:

Route PatternBehavior
/admin/*Requires admin role
/auth/*Redirects authenticated users to dashboard

All other routes require explicit session checks. The proxy is a first gate, not the only gate. Always verify with getSession() in components that handle sensitive data.

Server-Side Session Access

Use getSession() in React Server Components to check authentication:

Server Component

import { redirect } from 'next/navigation';
import { getSession } from '@kit/better-auth/context';
export default async function ProtectedPage() {
const session = await getSession();
if (!session) {
redirect('/auth/sign-in');
}
return <div>Hello, {session.user.name}</div>;
}

The function is cached per request using React's cache(). Multiple calls within the same request return the same result without additional database queries.

Session Data Structure

Session type

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 and organization context with automatic redirects, use getAccountContext():

Dashboard page

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

Account Context Structure

ServerAccountContext type

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 /home if not in organization 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:

Admin layout

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

The proxy already checks admin status, but re-verify in components for defense in depth. Session data could become stale between proxy check and component render.

Check Admin Status

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

Conditional admin link

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

Protected Server Actions

Use authenticatedActionClient to protect server actions:

Protected server action

'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. The client receives a proper error response.

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 className="h-8 w-8 rounded-full" />;
}
if (!session.data) {
return <SignInButton />;
}
return <Avatar name={session.data.user.name} />;
}

Session Hook Return Type

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

Always handle isPending to avoid layout shift. The hook fetches session state from the server on mount.

Signing Out

Use authClient.signOut() in client components:

Sign out button

'use client';
import { authClient } from '@kit/better-auth/client';
import { useRouter } from 'next/navigation';
export function SignOutButton() {
const router = useRouter();
const handleSignOut = async () => {
await authClient.signOut();
router.push('/');
};
return <button onClick={handleSignOut}>Sign out</button>;
}

This invalidates the session in the database and clears the session cookie.

Summary

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
authClient.signOut()Sign out userClient components

Common Pitfalls

  • Using getSession() in client components: It's server-only. Use authClient.useSession() for client-side session access. Importing it in a client component causes a build error.
  • Not handling isPending state: useSession() returns isPending: true while loading. Show a skeleton to avoid layout shift.
  • Assuming proxy protection is sufficient: Proxy checks happen at the edge and may have stale data. Re-verify in server components for sensitive operations.
  • Calling requireAdmin() without checking proxy first: If the proxy is misconfigured, non-admins could reach the page. Always have both layers.
  • Forgetting to protect server actions: Use authenticatedActionClient for all mutations that require authentication.
  • Multiple redirects from nested components: If multiple components call redirect(), you may get errors. Centralize auth checks in layouts.
  • Not caching expensive operations: getSession() is cached, but database queries in your components aren't. Use React's cache() for expensive operations.

Frequently Asked Questions

Is getSession() cached?
Yes. It uses React cache() so multiple calls in the same request return the same result without additional database queries.
What is the difference between getSession and getAccountContext?
getSession returns raw session data or null. getAccountContext returns user context with organization info and auto-redirects to sign-in if unauthenticated.
How do I protect API routes?
Use enhanceRouteHandler with auth: true, or call getSession() at the start of your handler and return 401 if null.
Can I access session data in middleware?
Yes. Use proxy.ts for route protection and getSession() in components for data access.
How long do sessions last?
Sessions expire after 7 days by default. This is configurable in the Better Auth configuration. See the Setup documentation.
How do I sign out a user programmatically?
Call authClient.signOut() on the client. On the server, use the Better Auth admin API to revoke sessions by user ID.
Where are sessions stored?
Session data is stored in the Session table in PostgreSQL via Prisma. Session tokens are stored in HTTP-only cookies.

Next: Personal Accounts