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.nameand 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=/,
HttpOnlyenabled, andSameSite=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.
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
getSessionaccepts 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.
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
SessionStorecontract below for production persistence.
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
maxEntriesto zero to disable persistence. A negative, infinite, or non-numeric limit throws.commit
maxAgeoverrides the cookiemaxAgefor 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.
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.redirectso 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.
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.