Session Management
Cookie-based sessions, the 5-minute cache, tRPC context creation, and session validation patterns.
Better Auth manages sessions as database rows in the session table, validated via a cookie on each request. This page covers how sessions are created, cached, validated, and how different parts of the codebase access them.
Session Table
Each session is a row in the session table (defined in packages/db/src/schema/auth.ts):
| Column | Type | Purpose |
|---|---|---|
id | TEXT (PK) | Session identifier |
token | TEXT (unique) | Cookie value |
userId | TEXT (FK) | Owner of the session |
activeOrganizationId | TEXT | Currently selected org (set by ensureActiveOrganization) |
expiresAt | TIMESTAMP | Expiration time |
ipAddress | TEXT | Client IP at creation |
userAgent | TEXT | Client user-agent at creation |
createdAt | TIMESTAMP | Creation time |
updatedAt | TIMESTAMP | Last modification time |
The session table is global (no RLS). Sessions resolve the user, not the tenant -- the tenant is resolved from activeOrganizationId after the session is validated.
Cookie Cache
Better Auth's cookie cache reduces database lookups during session validation:
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
When a session is validated, Better Auth caches the result in a signed cookie. For the next 5 minutes, session validation reads from the cookie without hitting the database. After 5 minutes, the next request performs a fresh database lookup and refreshes the cache.
This is a significant performance optimization: most page loads validate the session, and the dashboard makes multiple tRPC calls per page.
Reading the Session
There are two contexts for reading the session, each with different caching behavior:
In Server Components
Server components use the getSession() helper from apps/web/src/lib/auth-server.ts:
import { auth } from "@repo/auth/server";
import { headers } from "next/headers";
export async function getSession() {
return auth.api.getSession({ headers: await headers() });
}
This uses the cookie cache (5-minute TTL). For server component rendering, a slightly stale session is acceptable -- the user is viewing their own data.
In tRPC Context
The tRPC context in packages/api/src/context.ts disables the cookie cache for fresh reads:
const session = await auth.api.getSession({
headers: opts.headers,
query: { disableCookieCache: true },
});
This is intentional: tRPC mutations (creating plans, managing PATs, switching orgs) need the current session state, not a cached version. If a user switches organizations, the next tRPC call must see the new activeOrganizationId.
The Procedure Chain
After the session is resolved in the tRPC context, it flows through a middleware chain:
publicProcedure-- logs request timing; session may be nullprotectedProcedure-- throwsUNAUTHORIZEDif session is null; guaranteesctx.sessionis non-nulltenantProcedure-- readsactiveOrganizationIdfrom session; throwsPRECONDITION_FAILEDif null; wraps the handler inwithTenantContext()for RLSauthorizedProcedure-- looks up the user's member role, builds a CASL ability, attaches it to context
Most feature endpoints use authorizedProcedure. PAT endpoints use protectedProcedure because PATs are user-scoped (not tenant-scoped). See Data & Storage -- Query Patterns -- Procedure Chain for the full breakdown.
Rate Limiting
Better Auth applies rate limiting to all auth endpoints (sign-in, sign-out, session validation):
rateLimit: {
enabled: true,
window: 60, // 60 seconds
max: 30, // 30 requests per window
storage: "memory",
},
Rate limit state is stored in-memory (not Redis), which means it resets on server restart and is per-process in multi-process deployments. This is acceptable for the single-VM deployment model.
Session Expiration and Cleanup
Better Auth handles session expiration by checking expiresAt > now during validation. Expired sessions return null, triggering a redirect to the sign-in page.
Database cleanup of expired session rows is handled by Better Auth's internal maintenance. No application-level cron job is needed.
CSRF Protection
Better Auth validates the Origin header on mutating requests (POST, PUT, DELETE) against the configured baseURL. This prevents cross-site request forgery without requiring custom CSRF tokens.
Related Pages
- Sign-In Flow -- how sessions are created during OAuth
- Personal Access Tokens -- the alternative auth path for MCP (no sessions)
- Identity & Access -- Authorization -- how
authorizedProcedureuses the session to build CASL abilities - Infrastructure -- Secrets & Configuration -- Environment Variables --
BETTER_AUTH_SECRETandBETTER_AUTH_URL