Sessions
Think of it like… a coat-check counter. The server keeps the coat (your session data, sitting in a store). The cookie is the numbered, tamper-proof stub the browser hands back to claim it. If somebody forges the stub the signature won't match; if they steal the stub, rotating it on login or privilege change cancels the old one.
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 Workers, and Vercel 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.
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.
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.