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
Sessiontable 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 Pattern | Behavior |
|---|---|
/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
| Pattern | Use Case | Location |
|---|---|---|
getSession() | Get session data | Server components, server actions |
getAccountContext() | Get user context with redirect | Protected pages |
requireActiveOrganizationId() | Require org context | Org-only pages |
requireAdmin() | Require admin role | Admin pages/layouts |
isUserAdmin() | Check admin without redirect | Conditional rendering |
authenticatedActionClient | Protect server actions | Server actions |
authClient.useSession() | Client-side session | Client components |
authClient.signOut() | Sign out user | Client components |
Common Pitfalls
- Using
getSession()in client components: It's server-only. UseauthClient.useSession()for client-side session access. Importing it in a client component causes a build error. - Not handling
isPendingstate:useSession()returnsisPending: truewhile 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
authenticatedActionClientfor 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'scache()for expensive operations.
Frequently Asked Questions
Is getSession() cached?
What is the difference between getSession and getAccountContext?
How do I protect API routes?
Can I access session data in middleware?
How long do sessions last?
How do I sign out a user programmatically?
Where are sessions stored?
Next: Personal Accounts