Session Handling
Protect routes and access session data in loaders, server functions, and components. Use getSession, requireAdmin, and the authAction middleware factory.
Session Management
How to access and protect user sessions.
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 viacreateStart/createMiddleware/createCsrfMiddleware. beforeLoadguards (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 withnotFound()from authenticated non-admins), andredirectIfAuthenticated(keep signed-in users off the auth screens).
Attach a guard to a route or pathless layout route:
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,});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/action-middleware or an in-handler session check.
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:
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.
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:
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(),);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:
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:
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):
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 adminAction() middleware factory - 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:
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:
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 factories from @kit/action-middleware to protect server functions. authAction() pre-binds the authenticated middleware onto a createServerFn builder; continue the standard chain with .validator() and .handler(), and context.user is fully typed:
apps/web/src/lib/settings/update-profile.ts
import { z } from 'zod';import { authAction } from '@kit/action-middleware';const updateProfileSchema = z.object({ name: z.string().min(1),});export const updateProfileAction = authAction() .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 organizationAction() (requires an active organization - context.organizationId, context.role) and adminAction() / createAdminAction() (requires an admin). Call any of them from a client component with TanStack Query: useMutation({ mutationFn: updateProfileAction }).
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
| 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 |
authAction() | 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
requireAuthguard) - 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 authAction() (and friends) when:
- Building server functions that require authentication
- You want automatic error throwing if not authenticated
Frequently Asked Questions
Is getSession() called multiple times per request?
How do I sign out a user?
How long do sessions last?
Can I access the session in middleware?
What happens when a session expires?
Previous: Password Reset ← | Next: Multi-Factor Authentication →