Sessions
Sessions let you persist state across requests: authentication, user preferences, shopping carts, or any other server-side data you want tied to a client. Primate manages session cookies, validates session data, and ensures changes are committed only if your route succeeds.
Configuration
Sessions are configured in config/session.ts
. By default, Primate uses an
in-memory manager and a secure HTTP-only cookie named session_id
.
Session options
Option | Default | Description |
---|---|---|
cookie.httpOnly | true |
mark cookie as HttpOnly |
cookie.name | "session_id" |
name of the session cookie |
cookie.path | "/" |
path for which the cookie is valid |
cookie.sameSite | "Lax" |
SameSite cookie policy |
manager | InMemorySessionManager |
session manager instance |
schema | undefined |
validation schema for session data |
cookie.httpOnly
Whether the session cookie should be marked HttpOnly
(hidden from client-side
JavaScript).
cookie.name
The name of the session cookie.
cookie.path
The path for which the cookie is valid.
cookie.sameSite
The SameSite
cookie policy: "Strict"
, "Lax"
, or "None"
.
manager
The session manager. By default Primate uses an in-memory session manager that
resets on server restart. You can provide any custom manager that extends
SessionManager
.
schema
Optional schema to validate session data at runtime. Any schema with a
parse(input: unknown): T
method works. Primate recommends its own validation
library, Pema.
Example
Session facade
The session facade is the API you use in routes to interact with session state. It hides cookie handling and persistence details, exposing a simple interface to create, read, update, and destroy sessions.
method / property | description |
---|---|
id |
current session ID, if one exists |
exists |
whether a session is active |
create(initial) |
start a session with initial data; generates a new ID |
get() |
return current session data; throws if none |
try() |
return data if a session exists, otherwise undefined |
set(data) |
replace session data or derive from the previous state |
destroy() |
end the session and clear the cookie |
Usage in routes
Import the session facade via #session
. It is bound to the session data type
you declared in config/session.ts
.
import session from "#session";
import route from "primate/route";
route.get(() => {
if (!session.exists) {
session.create({ userId: 42 });
}
const data = session.get();
return `User ${data.userId} last active at ${data.lastActivity.toISOString()}`;
});
SessionFacade
reference
interface SessionFacade<T> {
readonly id: string | undefined;
readonly exists: boolean;
create(initial?: T): void;
get(): Readonly<T>;
try(): Readonly<T> | undefined;
set(next: ((previous: Readonly<T>) => T) | T): void;
destroy(): void;
}
Managers
A session manager is responsible for storing and retrieving session data. The default is in-memory and resets when the server restarts.
Primate leaves persistence to the manager. This contract defines the minimum required methods:
export default abstract class SessionManager<Data> {
init(): void | Promise<void> { }
abstract load(id: string): Data | undefined | Promise<Data | undefined>;
abstract create(id: string, data: Data): void | Promise<void>;
abstract save(id: string, data: Data): void | Promise<void>;
abstract destroy(id: string): void | Promise<void>;
}
Any manager that implements this interface can be plugged in through
config/session.ts
.
SessionManager
;
this is validated during start-up.Example: file-based manager
import is from "@rcompat/assert/is";
import FileRef from "@rcompat/fs/FileRef";
import type JSONValue from "@rcompat/type/JSONValue";
import SessionManager from "primate/session/Manager";
export default class FileSessionManager extends SessionManager<unknown> {
#directory: FileRef;
constructor(directory: string = "/tmp/sessions") {
is(directory).string();
super();
this.#directory = new FileRef(directory);
}
async init() {
await this.#directory.create({ recursive: true });
}
async load(id: string) {
is(id).uuid("invalid session id");
try {
return await this.#directory.join(id).json();
} catch {
return undefined;
}
}
async create(id: string, data: JSONValue) {
is(id).uuid("invalid session id");
await this.#directory.join(id).writeJSON(data);
}
async save(id: string, data: JSONValue) {
await this.create(id, data);
}
async destroy(id: string) {
is(id).uuid("invalid session id");
await this.#directory.join(id).remove();
}
}
Then configure:
import session from "primate/config/session";
import FileSessionManager from "./FileSessionManager.ts";
export default session({
manager: new FileSessionManager(),
});
Validation
When you provide a schema, Primate validates data passed to create
and set
:
import pema from "pema";
import string from "pema/string";
import session from "#session";
import route from "primate/route";
const Data = pema({ token: string.min(10) });
route.post(() => {
// Throws if token is shorter than 10 characters
session.set({ token: "abc" });
});
get
, try
or destroy
don't take input and do not
trigger validation.This ensures your session store never contains malformed data.