Navigated to /docs/core/sessions-and-authentication

Sessions and authentication

Store opaque signed sessions, rotate credentials, protect authentication boundaries, and keep request identity isolated during SSR.

Create session storage

Tavo.js stores only a signed opaque session ID in the cookie. Session data stays in a SessionStore such as a database or Redis adapter. The built-in memory store is bounded but process-local, so reserve it for tests and local development.

  • cookie.name and at least one non-empty secret are required. Every secret must contain at least 32 UTF-8 bytes.

  • Generate the signing secret once per environment, store it in the server environment, and share it across session-serving processes. Generating a new secret at every startup invalidates existing cookies.

  • Cookie defaults are Path=/, HttpOnly enabled, and SameSite=Lax. Secure is inferred from an HTTPS request unless explicitly configured.

  • Secrets are checked in array order and new cookies are signed with the first secret. Put the new secret first and retain old secrets during a rotation window.

TSsrc/server/sessions.ts
tsimport "@tavojs/core/server-only";
import { createSessionStorage } from "@tavojs/core/server";

const secret = process.env.SESSION_SECRET;
if (!secret) throw new Error("SESSION_SECRET is required");

export const sessions = createSessionStorage<{ userId?: string }>({
  cookie: {
    name: "__session",
    secrets: [secret],
    maxAge: 60 * 60 * 24 * 7,
    sameSite: "lax"
  }
  // Development uses the built-in memory store.
  // Add store: yourSessionStore for shared production persistence.
});

Session and storage API

  • getSession accepts a Request or an object with a request property. Missing, invalid, or expired cookies create a new empty session.

  • rotate replaces the opaque ID at the next commit and deletes the old store entry. Use it after login or a privilege change.

  • destroy marks the session for deletion. A later commit returns an expired cookie and removes the store entry.

  • redirect commits the session, appends Set-Cookie, normalizes the Location target, and defaults to status 303.

TS
tstype Session<T> = {
  readonly data: T;
  readonly id: string;
  readonly isNew: boolean;
  readonly rotated: boolean;
  readonly secure: boolean;
  get(key): T[key] | undefined;
  has(key): boolean;
  set(key, value): void;
  delete(key): void;
  rotate(): void;
  destroy(): void;
};

sessions.getSession(request): Promise<Session<T>>
sessions.commitSession(session, { maxAge? }): Promise<string>
sessions.destroySession(session, { maxAge? }): Promise<string>
sessions.redirect(to, session, init?): Promise<Response>

Commit login changes and expire logout cookies

Changing session data only updates the session object. Commit it and send the returned Set-Cookie header to persist the change. sessions.redirect does both and returns a 303 by default. The helper below receives a user ID only after your credential provider has verified the login; never take that ID directly from submitted form data.

  • Call these helpers from a server route action after its origin and method checks. Logout is a mutation; do not implement it as a GET loader.

  • A server middleware guard does not run during browser navigation. Every protected action and data endpoint must authorize its own request, and server-only pages need an appropriate browser data path or full document navigation.

  • The memory-store example loses sessions on restart and does not share them across Node processes. Use the SessionStore contract below for production persistence.

TSsrc/server/auth-response.ts
tsimport "@tavojs/core/server-only";
import { sessions } from "./sessions";

export async function finishVerifiedLogin(request: Request, verifiedUserId: string) {
  const session = await sessions.getSession(request);
  session.rotate();
  session.set("userId", verifiedUserId);
  return sessions.redirect("/account", session);
}

export async function logoutResponse(request: Request) {
  const session = await sessions.getSession(request);
  const cookie = await sessions.destroySession(session);
  return new Response(null, {
    status: 303,
    headers: { Location: "/login", "Set-Cookie": cookie }
  });
}

Custom and memory stores

  • The memory limit defaults to 10,000. Oldest entries are evicted when capacity is exceeded; expired entries are removed when read.

  • Set maxEntries to zero to disable persistence. A negative, infinite, or non-numeric limit throws.

  • commit maxAge overrides the cookie maxAge for that response. Expiry is stored alongside server data and serialized into the cookie.

  • Production stores must apply expiration consistently and support every runtime instance that can receive the user's next request.

TS
tstype SessionStore<T> = {
  get(id: string): MaybePromise<{ data: T; expiresAt: number | null } | null>;
  set(id: string, entry: { data: T; expiresAt: number | null }): MaybePromise<void>;
  delete(id: string): MaybePromise<void>;
};

const memory = createMemorySessionStore({ maxEntries: 10_000 });
memory.size();

Authenticate every request

This flow sketch uses sessions from the server module above and an application-supplied findUser function. Import them dynamically inside the server handlers in a shared route module. A missing session user ID must be treated as unauthenticated by your user lookup.

  • Read authentication in server middleware, loaders, actions, or plugin handlers for every protected request.

  • After verifying login, rotate the session ID, set the user ID, and return sessions.redirect so the cookie is committed.

  • Send only safe profile fields into loader data. Never expose the session ID, cookie, signing secret, or access token.

  • A client auth store may mirror safe user data after hydration, but the server must still authorize each request from the session backend.

TS
tsexport const middleware = defineServerMiddleware(async ({ request }) => {
  const session = await sessions.getSession(request);
  if (!session.get("userId")) return { redirect: "/login", status: 302 };
});

export const load = defineServerLoader(async ({ request }) => {
  const session = await sessions.getSession(request);
  const user = await findUser(session.get("userId"));
  return { user: user ? { id: user.id, name: user.name } : null };
});

Look up exact public types

Follow linked API names to their canonical TypeScript declarations and package boundaries.