- Cookie sessions for human users in browsers. A signed JWT sits in an
httpOnlycookie. The session record lives in your database so you can revoke it instantly. - JWT sessions for SPAs, mobile apps, and server-to-server flows. Stateless access tokens paired with rotating refresh tokens.
- Ephemeral agent sessions for AI agents (Claude, GPT-with-browsing, operator loops). Short-lived, budget-bounded credentials that expire by time or action count, whichever comes first.
Cookie sessions
1
Configure the session manager
PasscreateCookieSessionManager a config object and your theauth.db instance. The manager handles creation, validation, refresh, and revocation.lib/theauth.ts
2
Create a session after sign-in
CallcreateSession with a user ID after your authentication logic succeeds. createSession returns the session record and a ready-made Set-Cookie header value to send back to the browser. It does not return a Result: it resolves with the data or throws.routes/sign-in.ts
3
Validate on each request
Pass the rawCookie request header to validateSession. It returns { session, refreshCookieHeader }. session is null when the cookie is missing, invalid, expired, or revoked.middleware.ts
autoRefresh is on (the default), every successful validateSession call extends the session. Internally the old session row is deleted and a new one is created, so the session ID changes on each refresh. validateSession returns the new cookie as refreshCookieHeader. Forward it in your response.middleware.ts
4
Revoke on sign-out
Revoking a session removes it from the database immediately. Any subsequent validation attempt returnssession: null.routes/sign-out.ts
JWT sessions
Use JWT sessions when cookies don’t work: SPAs calling a separate API origin, mobile apps, server-to-server flows. Access tokens are short-lived and stateless. Refresh tokens are opaque random strings stored as SHA-256 hashes, and they rotate on every use.lib/theauth.ts
customClaims receives only { id, email?, name? }. On refresh the module re-issues tokens from { id } alone, so claims that depend on email or name are not present on tokens minted by refreshSession.
Create a session
routes/sign-in.ts
Verify on each request
Access token verification is stateless. No database round-trip.middleware.ts
Refresh
CallingrefreshSession marks the old refresh token as used and issues a new access + refresh pair. Each refresh rotates the token, so a stolen token is invalidated the moment the legitimate client refreshes.
routes/refresh.ts
Session freshness
Some operations should require that the user authenticated recently, not just that they hold any valid session. Changing a password, registering a passkey, or modifying billing details are examples where a session from 6 days ago is not good enough even though it is technically valid.createSessionFreshnessModule wraps this check. When a session is older than freshAge seconds, it returns a 403 response with code SESSION_NOT_FRESH. Your handler returns it directly.
lib/freshness.ts
routes/change-password.ts
SESSION_NOT_FRESH, redirect them to a lightweight re-authentication page (password confirmation, passkey prompt, or similar) rather than a full sign-out. After re-auth succeeds, refresh the session’s createdAt timestamp by issuing a new session, then retry the original operation.
The
freshAge threshold is separate from maxAge. A session can be well within its 7-day lifetime but still be considered stale for sensitive operations. Keep freshAge short. 5 to 15 minutes is typical.CSRF protection
Cookie-based sessions are vulnerable to cross-site request forgery unless you add a second layer. theAuth uses the double-submit cookie pattern: a random token is set in a separate readable cookie and must also appear in the request body or header. An attacker’s page can trigger the cookie but cannot read it to reproduce the header value.lib/csrf.ts
Revocation patterns
- Single session
- All sessions
- All except current
Revoke one session
Session metadata
Store arbitrary data on a session at creation time. Useful for tracking the device, IP address, or a custom attribute your app needs. The object you pass as the second argument ofcreateSession is stored as the session metadata. There is no separate call to update it later.
Creating a session with metadata
Reading session metadata
Metadata is stored as a JSON column. It is not indexed, so avoid querying sessions by metadata fields. If you need to look up sessions by device or IP, store those in a separate indexed column via a custom schema extension.
Human sessions vs agent tokens
Agent tokens use a
kv_ prefix and are stored only as a SHA-256 hash. The raw token is shown once at creation and cannot be recovered. If a token is lost, issue a new one.
Error codes
The cookie session manager does not return error codes:validateSession returns session: null for a missing, invalid, expired, or revoked cookie. The JWT session module and the helpers return the following:
validateCsrfToken and validateOrigin return { valid: false, reason } rather than a code. The status codes in the table are what the modules suggest, not something the library sets on a response (except the 403 from freshness.guard).
Configuration reference
CookieSessionConfig
string
required
Signing secret for the session token, at least 32 characters.
string
default:"'theauth_session'"
Name of the session cookie.
number
default:"604800 (7 days)"
Session lifetime in seconds. After this period the session is considered expired even if it has been used recently.
boolean
default:"true"
When true, every successful
validateSession extends the session by replacing the session row (the session ID changes) and returns a new cookie as refreshCookieHeader.boolean
default:"true"
Prevents client-side JavaScript from accessing the cookie. Always set this to true for session cookies.
boolean
Only send the cookie over HTTPS. Set it to true explicitly if your deployment does not set NODE_ENV=production.
'lax' | 'strict' | 'none'
default:"'lax'"
Controls cross-site cookie behavior. ‘lax’ works for most apps. ‘strict’ adds more protection. ‘none’ is only for cross-origin cookie transport (requires secure: true).
string
default:"'/'"
Restricts the cookie to a URL path prefix.
string
default:"undefined"
Set a cookie domain to share sessions across subdomains. Omit for single-domain apps.
JwtSessionConfig
string | CryptoKey | JsonWebKey
required
Signing secret. A string uses HMAC-SHA256 and must be at least 32 characters. Pass a CryptoKey or JsonWebKey for asymmetric algorithms (RS256, ES256).
string
default:"'HS256'"
JWT signing algorithm. Inferred from the secret type when not set: ‘HS256’ for strings, ‘RS256’ for RSA keys, ‘ES256’ for EC keys.
number
default:"900 (15 min)"
Access token lifetime in seconds. Keep this short. Access tokens are stateless and cannot be revoked before expiry.
number
default:"604800 (7 days)"
Refresh token lifetime in seconds. Refresh tokens are stored hashed and can be revoked immediately.
string
default:"undefined"
Value for the JWT ‘iss’ claim. Validated on every verify call.
string
default:"undefined"
Value for the JWT ‘aud’ claim. Validated on every verify call.
(user: { id: string; email?: string; name?: string }) => Record<string, unknown>
default:"undefined"
Function called at token creation to attach extra claims to the access token payload.
SessionFreshnessConfig
number
default:"300 (5 min)"
Maximum session age in seconds before a sensitive operation requires re-authentication. The freshness guard returns 403 SESSION_NOT_FRESH when the session is older than this.
Security best practices
Set
cookieOptions.sameSite: 'lax' for most apps. 'lax' allows the cookie to be sent on top-level navigations (clicking a link) but blocks it on cross-origin subresource requests, which stops most CSRF attacks without breaking OAuth redirect flows. Only use 'strict' if you have no external links that expect to land in an authenticated state.Short access token TTLs (
accessTokenTtl) reduce the window of exposure for a stolen token, but they increase refresh traffic. 15 minutes is a reasonable default. If you need to revoke access instantly (account ban, credential compromise), route your API through the session validation middleware instead of relying solely on stateless JWT verification.Related
Cookie options
Cookie attributes, cross-subdomain setup, and rolling vs absolute expiry.
JWT sessions
Stateless access tokens with rotating refresh tokens.
Multi-session
Cap concurrent sessions and build an active devices settings page.
Ephemeral sessions
Short-lived, budget-bounded agent credentials for single tasks.