# Session Handling

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

*Canonical: https://makerkit.dev/docs/nextjs-prisma/authentication/session-handling*

---

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](./overview).

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 platform 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 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:

```typescript {% title="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

```typescript {% title="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()`:

```typescript {% title="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

```typescript {% title="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:

```typescript {% title="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:

```typescript {% title="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:

```typescript {% title="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:

```typescript {% title="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:

```tsx {% title="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

```typescript
{
  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:

```tsx {% title="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. 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.

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

---

**Next:** [Personal Accounts](../personal-accounts/overview)
