Skip to content

Sessions & guards

Sessions are DB-backed, not JWT — logout revokes the session server-side instantly. The flow:

  • createSession(userId) generates a 256-bit random token; the DB stores only its SHA-256 hash (hashToken). A DB leak cannot expose valid tokens.
  • The raw token lives in the session cookie: httpOnly, SameSite=Lax, Secure in production, 30-day expiry.
  • resolveUser(token) looks up the session, lazily deletes it when expired, and returns the user row.
  • Sessions are rotated on login/register (session-fixation defense) and password changes delete other sessions for the user.

One-shot flash messages live on the session row (sessions.flash), set via setFlash(token, { success | error }), merged into the next Inertia page payload, and cleared on render.

Guards are Hono middleware — return a Response to short-circuit, or await next():

app.get("/dashboard", requireAuth, (c) => c.var.inertia.render("Dashboard", {}));
app.get("/admin", requireRole("admin"), (c) => ...);
Guard Redirects to
requireAuth /login when signed out
guestOnly /dashboard when signed in
requireRole("admin") /login (guest) or /dashboard (non-admin)

SameSite=Lax cookie + an Origin check on unsafe methods (non-browser clients that omit Origin are allowed). Cross-origin POST/PUT/PATCH/DELETE with a mismatched host get 403.

Two layers of in-memory fixed-window rate limiting, both keyed by X-Forwarded-For/peer IP (Bun’s server.requestIP via c.env):

  1. Global (app.ts) — DDoS baseline on all routes, excludes /health, /assets/*, and /.well-known/*. Configure via RATE_LIMIT_GLOBAL_MAX / RATE_LIMIT_GLOBAL_WINDOW (default 200/60s).
  2. Auth (auth.routes.ts) — stricter layer on auth endpoints (brute-force protection). Configure via RATE_LIMIT_AUTH_MAX / RATE_LIMIT_AUTH_WINDOW (default 30/60s).

See Rate limiting for the full design — client identification, memory management, and the sub-app path filter bug.