class

Kemal::Session

Inherits Reference < Object

A cookie-store session for Kemal: the entire session lives inside a single AES-256-GCM encrypted, tamper-proof cookie. Nothing is stored server-side — there is no storage engine, no session id registry, and no GC.

Unlike kemal-session (a server-side store keyed by a signed id), this keeps all session data in the cookie itself; the public value API is kept compatible with kemal-session. As a bonus, the cookie's wire format is byte-compatible with Rails' ActionDispatch::Session::CookieStore, so a Kemal and a Rails 8 app can share sessions.

The session hash is flat, so a key lives in a single namespace rather than one-per-type: session.int("n", 1) and session.string("n", "x") refer to the same key "n".

Constants

MAX_COOKIE_SIZE = 4096

The largest cookie browsers are required to accept, and the limit Rails enforces (ActionDispatch::Cookies::MAX_COOKIE_SIZE).

VERSION = "0.1.0"

Constructors

new(context : HTTP::Server::Context)

Builds a session from a request context, decrypting the incoming cookie (if present and valid). An invalid/tampered cookie yields an empty session.

Source
new

Builds a detached, empty session (no cookie is written).

Source

Class methods

config

Yields the config for block-style setup: Kemal::Session.config { |c| c.secret = "..." }.

Source
config

Returns the config singleton.

Source

Instance methods

bool(key : String, value : Bool) : Bool
Source
bool(key : String) : Bool
Source
bool?(key : String) : Bool | Nil
Source
commit

Writes the cookie for this request, if the session was mutated, and marks the session as sent — after which further mutations raise ResponseAlreadySent.

Session::Handler calls this immediately before the first byte of the response body (the last moment the Set-Cookie header can still change), and again for responses that end without a body. Calling it twice is harmless: the second call has nothing to write.

Source
delete(key : String) : Nil

Removes a key from the session (regardless of value type) and persists.

Source
destroy

Clears the session and expires the cookie. The cookie stays expired for the rest of the request: anything that repopulates the store afterwards (a layout rendering csrf_meta_tags after a logout, say) would otherwise save a fresh live cookie and undo the call.

Source
destroyed?

Whether #destroy was called on this session. A destroyed session still works in memory for the rest of the request, but never writes a live cookie again — only #reset starts a new one.

Source
dirty?

Whether the store has been mutated since the cookie was last written. Mutations only mark the session dirty; Session::Handler encrypts and writes the cookie once per request (see #commit).

Source
encode

The raw (unescaped) encrypted cookie value for the current store.

Source
float(key : String, value : Float64) : Float64

float stays strict: a whole number is an integer, not a float. Widening the guard to Int | Float would break the guard symmetry the flat store relies on, since int? would still reject 5.0.

Source
float(key : String) : Float64

float stays strict: a whole number is an integer, not a float. Widening the guard to Int | Float would break the guard symmetry the flat store relies on, since int? would still reject 5.0.

Source
float?(key : String) : Float64 | Nil

float stays strict: a whole number is an integer, not a float. Widening the guard to Int | Float would break the guard symmetry the flat store relies on, since int? would still reject 5.0.

Source
id

The session_id. Generated (and persisted to the cookie) on first access if not already present.

Source
int(key : String, value : Int64) : Int64

int is Int64 — the width JSON integers already have in the store — so no stored integer is ever too wide to read back.

Source
int(key : String) : Int64

int is Int64 — the width JSON integers already have in the store — so no stored integer is ever too wide to read back.

Source
int?(key : String) : Int64 | Nil

int is Int64 — the width JSON integers already have in the store — so no stored integer is ever too wide to read back.

Source
object(key : String, value : T) : T forall T

Stores any JSON-serializable value (a JSON::Serializable object, an Array/Hash of them, or a primitive) under key and persists.

session.object("cart", cart)
Source
object(key : String, *, as type : T.class) : T forall T

Reads back a complex value, reconstructing it as type. as is a keyword argument so this never collides with the writer overload. Raises KeyError if the key is absent.

session.object("cart", as: Cart)
session.object("items", as: Array(Item))
Source
object?(key : String, *, as type : T.class) : T | Nil forall T

Like #object, but returns nil when the key is absent or the stored value cannot be reconstructed as type.

Source
reset

Clears all data and starts a fresh session (new session_id). This deliberately starts a new session, so it lifts a previous #destroy.

Source
save

Serializes and re-encrypts the session into the response cookie. Prefer letting Session::Handler do this once per request via #commit; calling it directly writes the cookie immediately, which only works while the response body has not started. A no-op for detached sessions.

A session that has become empty — or that was destroyed — expires the cookie rather than leaving the last value the client was given in place; otherwise deleting the final key (a hand-rolled logout) would silently keep the old session alive.

Raises CookieOverflow if the session no longer fits in a cookie. The dirty flag is cleared first, so an overflowing session raises once, from whichever call tried to write it, rather than again from every later commit.

Source
sent?

Whether the cookie for this request has already been handed to the response. Past that point the Set-Cookie header is fixed, so further mutations raise rather than being lost.

Source
store

The underlying flat session hash. Values Rails would set are visible here and vice-versa.

Source
string(key : String, value : String) : String
Source
string(key : String) : String
Source
string?(key : String) : String | Nil
Source

Macros

typed_accessor(name, type, guard, read, write)

Generates, for a given name/Crystal-type: name(k) -> value (raises KeyError if absent/wrong type) name?(k) -> value? (nil if absent/wrong type) name(k, v) -> v (set + persist)

Deletion is type-agnostic (the store is flat): see #delete.

Source

Nested types