Sessions
Session data stays in the configured store. The browser receives a signed identifier, and rotating that identifier after login or a privilege change invalidates the previous session reference.
DaloyJS ships a small, runtime-portable session() middleware: a signed__Host- cookie carries the session id, the payload lives in a pluggable SessionStore (in-memory by default, KV / Redis-shaped stores plug in directly), and per-request mutations are exposed on ctx.state.session. There are no adapter-specific code paths - the same middleware runs on Node, Bun, Deno, Cloudflare and Workers because it only uses WebCrypto and standard Set-Cookie headers.
- 01requestBrowsersession()Request carries the signed __Host- cookieCookie: __Host-daloy.sid=<id>.<hmac>
- 02notesession()BrowserBad / forged signature is rejected, no data loadedHMAC-SHA256 mismatch to empty session
- 03requestsession()StoreValid signature loads the server-side payloadstore.get(id)
- 04asyncHandlersession()On login: regenerate() issues a fresh idold store record destroyed to defeat fixation
- 05responsesession()BrowserPersist once in onSend, re-emit the cookieSet-Cookie: HttpOnly; Secure; SameSite=Lax
Quick start
Defaults
Every option is conservative by default, with explicit error messages when a setting would silently weaken security (for example, a non-/ path on a __Host- cookie or SameSite=None without Secure).
cookieName:__Host-daloy.sid- forcesSecure,Path=/, noDomain.cookieOptions:{ secure: true, httpOnly: true, sameSite: "Lax", path: "/", maxAgeSeconds: 86_400 }.store: a freshMemorySessionStore()per app. Replace with a KV-backed store in production.rolling:true- every authenticated request slides the expiry and re-emitsSet-Cookie.saveUninitialized:false- anonymous traffic that never touches the session never writes a cookie or store record.generateId:crypto.randomUUID()when available, otherwise a base64url-encoded 32-byte random string. Pass your owngenerateIdto customize.
The session API
Inside a handler, ctx.state.session exposes:
id: string- current session id.data: Record<string, unknown>- payload object. Mutating it throughset/deletemarks the session dirty and triggers a single store write inonSend.get<T>(key)/set(key, value)/delete(key).regenerate({ keepData? })- issues a new id, destroys the previous store record, and (by default) carries the existing payload over. Call it on login and on privilege escalation to defend against session fixation.destroy()- drops server-side state and emits aSet-CookiewithMax-Age=0. A destroyed session cannot be resurrected by a request that was already in flight when logout ran (since 1.3.7): write-backs of a session loaded from the cookie only succeed if the record still exists, and the middleware remembers ids it destroyed (bounded, for the session TTL) so a concurrent request skips both the store write and the cookie refresh.
Automatic rotation on privilege changes
rotateSession() watches privilege-bearing session values and calls session.regenerate() after the handler if they changed. The default watch list covers userId, tenantId, roles, scopes, and isAdmin. If a handler already calls regenerate(), the helper skips itself.
Key rotation
Pass an array to secret. The first entry is always used to sign new cookies. Any later entry can verify (so older clients keep working until their next request) and triggers a transparent re-sign on the way out.
Pluggable store
Implement SessionStore against any KV/Redis-shaped backend. Methods may return synchronously or via a Promise - DaloyJS always awaits them, so a fully async store works without changes.
Custom stores should implement update() as an atomic conditional write. Stores without it still work: before writing back a loaded session, session() re-reads it with get() and skips the write when it is gone, remembers ids this instance destroyed, and logs a one-time warning at setup. That makes logout revocation complete within one process. When several instances share the store, a narrow window remains between that get() and set() where an in-flight request on another instance can restore a session that was just destroyed, so multi-instance deployments should implement update().
update(sid, record)(optional, since 1.3.7) - an atomic conditional write: overwrite the record only if it still exists and is unexpired, returningtruewhen written andfalsewhen it was missing. The middleware uses it for every write-back of a session loaded from a cookie. RedisSET ... XXis one way to make this atomic.touch(sid, expiresAt)- slides the expiry for rolling sessions. If provided, it must atomically extend only an existing record, never create one. When omitted, rolling refresh usesupdate().
With Redis, XX makes update() atomic and PEXPIRE on an existing key makes touch() safe by construction:
Standalone signing helpers
The same HMAC-SHA256 primitives that power the cookie are exported as signValue(value, secret) and verifySignedValue(signed, secret) (which accepts a single secret or an array for rotation). Use them for ad-hoc cookies, magic links, or any other place you need a tamper-evident token without standing up the full session pipeline.
Security notes
- The session cookie is HttpOnly by default - it is unreadable from JavaScript. Pair it with the
csrf()middleware on mutating routes. - Always rotate the id with
regenerate()on login and privilege escalation. - Use
destroy()on logout to invalidate both the cookie and the store record. - Treat the
secretarray as append-only: when you rotate, prepend the new key and keep the previous entry until the longest plausible session has expired. - The default
MemorySessionStoreis per-process - it is suitable for tests and single-instance deployments only. Use a KV/Redis-shaped store across replicas, and implementupdate()there so logout cannot be undone by a concurrent request on another instance.