# Session Handling

> Protect routes and access session data in loaders, server functions, and components. Use getSession, requireAdmin, and the authFunctionMiddleware tuple.

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

---

{% sequence title="Session Management" description="How to access and protect user sessions." %}

[Server-side session access](#server-side-session-access)

[Client-side session access](#client-side-session)

[Protect server functions](#protected-server-functions)

[Admin protection](#admin-protection)

{% /sequence %}

Sessions are stored in the database and referenced via HTTP-only cookies. The kit provides multiple utilities for accessing session data and protecting routes based on your use case.

Routes are protected with `beforeLoad` guards, not a Next.js-style middleware. For data and endpoints, always authorize the session inside the loader or server function.

## Route-Level Protection

There is no proxy or `middleware.ts`. Two layers protect the app:

- **Global request middleware** (`apps/web/src/start.ts`) wires CSRF protection (scoped to server functions) and security headers via `createStart` / `createMiddleware` / `createCsrfMiddleware`.
- **`beforeLoad` guards** (`apps/web/src/lib/auth/guards.ts`) gate navigation per route group: `requireAuth` (redirect anonymous users to sign-in), `requireAdminAuth` (redirect anonymous to sign-in, hide the admin surface with `notFound()` from authenticated non-admins), and `redirectIfAuthenticated` (keep signed-in users off the auth screens).

Attach a guard to a route or pathless layout route:

```typescript {% title="apps/web/src/routes/_authenticated/route.tsx" %}
import { createFileRoute } from '@tanstack/react-router';

import { requireAuth } from '#/lib/auth/guards';

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: requireAuth,
});
```

{% callout type="warning" title="Guards gate navigation, not data" %}
A `beforeLoad` guard keeps a user out of a screen; it does not protect the endpoints the screen calls. Every server function and loader that reads or writes private data must also authorize itself - via `@kit/function-middleware` or an in-handler session check.
{% /callout %}

## Server-Side Session Access

Use `getSession()` from `@kit/better-auth/context` to read the current session inside a server function or loader. It reads the request headers, so it only runs on the server:

```typescript {% title="apps/web/src/lib/auth/session.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { getSession } from '@kit/better-auth/context';

export const fetchSession = createServerFn({ method: 'GET' }).handler(() =>
  getSession(),
);
```

Wrapping `getSession()` in a `createServerFn` lets `beforeLoad`/`loader` read the session even during client-side navigation, where `getRequestHeaders()` is unavailable. `getSession()` is memoized per request (a `WeakMap` keyed on the request headers), so multiple calls within the same request are efficient.

```typescript {% title="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 the user context (personal vs organization), use `getAccountContext()`. It calls `requireSession()` internally and **throws** if there is no session, so call it from a server function behind a `requireAuth` guard:

```typescript {% title="apps/web/src/lib/auth/account.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { getAccountContext } from '@kit/better-auth/context';

export const fetchAccountContext = createServerFn({ method: 'GET' }).handler(
  () => getAccountContext(),
);
```

```tsx {% title="apps/web/src/routes/_authenticated/dashboard/route.tsx" %}
import { createFileRoute } from '@tanstack/react-router';

import { fetchAccountContext } from '#/lib/auth/account.functions';

export const Route = createFileRoute('/_authenticated/dashboard')({
  loader: () => fetchAccountContext(),
  component: DashboardPage,
});

function DashboardPage() {
  const context = Route.useLoaderData();

  if (context.isOrganization) {
    return <OrgDashboard orgId={context.activeOrganizationId} />;
  }

  return <PersonalDashboard user={context.user} />;
}
```

The `getAccountContext` function returns the following structure:

```typescript {% title="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 loaders that require an active organization, `requireActiveOrganizationId()` throws a `redirect` to the app home path when no organization is active:

```typescript {% title="members.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { requireActiveOrganizationId } from '@kit/better-auth/context';

export const fetchMembers = createServerFn({ method: 'GET' }).handler(
  async () => {
    // Redirects to the app home path if not in org context
    const orgId = await requireActiveOrganizationId();

    return loadMembers(orgId);
  },
);
```

## Admin Protection

### Require Admin in Routes

Gate the admin surface with the `requireAdminAuth` guard in `beforeLoad`. It redirects anonymous users to sign-in and throws `notFound()` for an authenticated non-admin (hiding the surface rather than leaking a 403):

```typescript {% title="apps/web/src/routes/admin/route.tsx" %}
import { createFileRoute } from '@tanstack/react-router';

import { requireAdminAuth } from '#/lib/auth/guards';

export const Route = createFileRoute('/admin')({
  beforeLoad: requireAdminAuth,
  // ... layout loader + component
});
```

The guard delegates to `requireAdmin()` semantics (`@kit/auth/require-admin`). Because the guard only controls navigation, **also authorize admin server functions** with the `createServerFn({ method: 'POST' }).middleware(adminFunctionMiddleware)` middleware tuple - never rely on the guard alone.

### Check Admin Status

Use `isUserAdmin()` when you need to check admin status without redirecting. Expose it to the UI through a server function:

```typescript {% title="apps/web/src/lib/auth/session.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { isUserAdmin } from '@kit/auth/require-admin';

export const fetchAuthState = createServerFn({ method: 'GET' }).handler(
  async () => ({ isAdmin: await isUserAdmin() }),
);
```

In a component, render the admin link with `Link` from `@tanstack/react-router`:

```tsx
import { Link } from '@tanstack/react-router';

<nav>
  <Link to="/dashboard">Dashboard</Link>
  {isAdmin && <Link to="/admin">Admin</Link>}
</nav>;
```

## Protected Server Functions

Use the middleware tuples from `@kit/function-middleware` to protect server functions. `createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware)` pre-binds the authenticated middleware onto a `createServerFn` builder; continue the standard chain with `.validator()` and `.handler()`, and `context.user` is fully typed:

```typescript {% title="apps/web/src/lib/settings/update-profile.ts" %}
import { z } from 'zod';

import { createServerFn } from '@tanstack/react-start';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';

const updateProfileSchema = z.object({
  name: z.string().min(1),
});

export const updateProfileFunction = createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware)
  .validator(updateProfileSchema)
  .handler(async ({ data, context }) => {
    // context.user is guaranteed to exist
    await updateUser(context.user.id, data);

    return { success: true };
  });
```

The middleware throws an "Unauthorized" error if the session is invalid. The package also exports `createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)` (requires an active organization - `context.organizationId`, `context.role`) and `createServerFn({ method: 'POST' }).middleware(adminFunctionMiddleware)` / `withAdminPermission()` (requires an admin). Call any of them from a client component with TanStack Query: `useMutation({ mutationFn: updateProfileFunction })`.

## 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 />;
  }

  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;
}
```

## Summary

| Function | Use Case | Redirects? |
|----------|----------|------------|
| `getSession()` | Get session data in server contexts | No |
| `requireSession()` | Get session or throw error | No (throws) |
| `getAccountContext()` | Get user context for protected pages | No (throws if anonymous) |
| `requireActiveOrganizationId()` | Require org context | Yes (to app home path) |
| `requireAuth` guard | Gate a route to authenticated users | Yes (to sign-in) |
| `requireAdmin()` / `requireAdminAuth` guard | Require admin role | Yes (sign-in) / 404 for non-admins |
| `isUserAdmin()` | Check admin without redirect | No |
| `createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware)` | Protect server functions | No (throws) |
| `authClient.useSession()` | Client-side session | No |

## When to Use Each Pattern

**Use `getSession()` when:**
- You need to check if a user is logged in
- You want to handle the unauthenticated state yourself
- You're inside a server function or loader

**Use `getAccountContext()` when:**
- Loading data for a protected page (behind a `requireAuth` guard)
- You need to know if the user is in personal or organization context

**Use `requireActiveOrganizationId()` when:**
- The loader requires an active organization
- You want an automatic redirect to the app home path if no org is selected

**Use the `requireAdminAuth` guard / `requireAdmin()` when:**
- Gating admin-only routes
- You want anonymous users sent to sign-in and non-admins hidden with a 404

**Use `createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware)` (and friends) when:**
- Building server functions that require authentication
- You want automatic error throwing if not authenticated

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Is getSession() called multiple times per request?", "answer": "getSession() is memoized per request via a WeakMap keyed on the request headers, so multiple calls within the same request share the same result. You can call it freely without performance concerns."},
     {"question": "How do I sign out a user?", "answer": "Use authClient.signOut() from the client. This clears the session cookie and removes the session from the database. The user is then redirected to the sign-in page."},
     {"question": "How long do sessions last?", "answer": "Sessions are configured in Better Auth. By default, sessions expire after a period of inactivity. The exact duration depends on your configuration."},
     {"question": "Can I access the session in middleware?", "answer": "Route protection uses TanStack Router beforeLoad guards (apps/web/src/lib/auth/guards.ts) plus global request middleware in apps/web/src/start.ts. Read the session inside a server function or loader via getSession(); guards call it through the fetchSession server function."},
     {"question": "What happens when a session expires?", "answer": "The next request with an expired session returns null from getSession(). Client components using useSession() will update automatically. Protected pages will redirect to sign-in."}
   ]
/%}

---

**Previous:** [Password Reset ←](./password-reset) | **Next:** [Multi-Factor Authentication →](./mfa)
