Trovella Wiki

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

ColumnTypePurpose
idTEXT (PK)Session identifier
tokenTEXT (unique)Cookie value
userIdTEXT (FK)Owner of the session
activeOrganizationIdTEXTCurrently selected org (set by ensureActiveOrganization)
expiresAtTIMESTAMPExpiration time
ipAddressTEXTClient IP at creation
userAgentTEXTClient user-agent at creation
createdAtTIMESTAMPCreation time
updatedAtTIMESTAMPLast 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.

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:

  1. publicProcedure -- logs request timing; session may be null
  2. protectedProcedure -- throws UNAUTHORIZED if session is null; guarantees ctx.session is non-null
  3. tenantProcedure -- reads activeOrganizationId from session; throws PRECONDITION_FAILED if null; wraps the handler in withTenantContext() for RLS
  4. authorizedProcedure -- 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.

On this page