brazier/session

Types

Storage operations for an External backend. Each returns Error(reason) when the storage fails; Brazier logs it and returns SessionBackendFailed(reason). Every operation must be atomic on its own; Brazier builds updates and rotation from them.

pub type ExternalBackend {
  ExternalBackend(
    name: String,
    create: fn(StoredSession) -> Result(Bool, String),
    fetch: fn(String) -> Result(
      option.Option(StoredSession),
      String,
    ),
    touch: fn(String, Int) -> Result(Bool, String),
    replace: fn(StoredSession, Int) -> Result(Bool, String),
    delete: fn(String) -> Result(Nil, String),
    delete_version: fn(String, Int) -> Result(Bool, String),
    expired: fn(Int, Int, Int) -> Result(
      List(#(String, Int)),
      String,
    ),
    count: fn() -> Result(Int, String),
  )
}

Constructors

  • ExternalBackend(
      name: String,
      create: fn(StoredSession) -> Result(Bool, String),
      fetch: fn(String) -> Result(
        option.Option(StoredSession),
        String,
      ),
      touch: fn(String, Int) -> Result(Bool, String),
      replace: fn(StoredSession, Int) -> Result(Bool, String),
      delete: fn(String) -> Result(Nil, String),
      delete_version: fn(String, Int) -> Result(Bool, String),
      expired: fn(Int, Int, Int) -> Result(
        List(#(String, Int)),
        String,
      ),
      count: fn() -> Result(Int, String),
    )

    Arguments

    name

    Names the store in logs and in the live-connection registry. Every node using the same storage must use the same name.

    create

    Insert a new row. Ok(False) when the id already exists.

    fetch

    The row for an id, Ok(None) when there is none. Lifetimes are checked by Brazier.

    touch

    Set a row’s last_seen_ms. Ok(False) when the row is gone. Must not create a row.

    replace

    Replace the row with the same id only if its version is still the given one (compare-and-swap). Ok(False) when it is not.

    delete

    Delete a row. Deleting a missing row succeeds.

    delete_version

    Delete a row only if its version is still the given one.

    expired

    Up to limit #(id, version) pairs of rows created before the first cutoff or last seen before the second.

    count

    The number of rows.

Why a bound process was told its session is gone.

pub type RevocationReason {
  Deleted
  Rotated
  Expired
}

Constructors

  • Deleted

    The session was deleted (logout), or was found missing when the bindings were re-checked after a node connected.

  • Rotated

    The session id was replaced by rotate (login or privilege change).

  • Expired

    The session’s absolute or idle lifetime passed.

A session with key-value data.

pub type Session {
  Session(id: String, data: dict.Dict(String, String))
}

Constructors

  • Session(id: String, data: dict.Dict(String, String))

    Arguments

    id

    Unique session identifier (stored in cookie).

    data

    Session data (key-value pairs).

Where a store keeps its sessions.

pub type SessionBackend {
  LocalEts(name: String)
  External(backend: ExternalBackend)
}

Constructors

  • LocalEts(name: String)

    An ETS table on this node. Sessions are not shared between nodes.

  • External(backend: ExternalBackend)

    Functions over shared storage, used the same way from every node.

A session store handle.

pub opaque type SessionStore

Why a session operation failed.

pub type SessionStoreError {
  SessionNotFound
  SessionExpired
  SessionContention
  SessionBackendFailed(reason: String)
  InvalidBackend(reason: String)
}

Constructors

  • SessionNotFound

    The session does not exist: never created, deleted, or rotated away.

  • SessionExpired

    The session existed but its absolute or idle lifetime has passed.

  • SessionContention

    The compare-and-swap lost too many races in a row. This indicates a pathological write storm on one session and is reported, never hidden.

  • SessionBackendFailed(reason: String)

    An External backend operation failed.

  • InvalidBackend(reason: String)

    The backend configuration is invalid.

Opaque ETS table reference.

pub type SessionTable

Lifetime configuration for a session store.

pub type StoreConfig {
  StoreConfig(
    absolute_ttl_ms: Int,
    idle_ttl_ms: Int,
    sweep_interval_ms: Int,
  )
}

Constructors

  • StoreConfig(
      absolute_ttl_ms: Int,
      idle_ttl_ms: Int,
      sweep_interval_ms: Int,
    )

    Arguments

    absolute_ttl_ms

    Maximum age of a session from creation, in milliseconds.

    idle_ttl_ms

    Maximum time between accesses, in milliseconds.

    sweep_interval_ms

    How often the background sweeper deletes expired sessions.

A session row as a backend stores it. version increases on every data write; times are erlang:system_time(millisecond).

pub type StoredSession {
  StoredSession(
    session: Session,
    version: Int,
    created_at_ms: Int,
    last_seen_ms: Int,
  )
}

Constructors

  • StoredSession(
      session: Session,
      version: Int,
      created_at_ms: Int,
      last_seen_ms: Int,
    )

Values

pub fn bind_current_process(
  store: SessionStore,
  session_id: String,
) -> Nil

Bind the calling process (a WebSocket handler) to a session.

When the session is deleted, rotated, or expires (on any connected node), the process receives {brazier_session_revoked, SessionId, ReasonBinary}. Binding is idempotent and is released automatically when the process exits.

pub fn binding_fingerprint(session_id: String) -> String

Lowercase hex SHA-256 of a session id.

Embed this (never the raw id, which is a bearer secret) in signed tokens that must be bound to a session, such as the WebSocket join token.

pub fn bound_process_count(
  store: SessionStore,
  session_id: String,
) -> Int

How many live processes on this node are bound to a session id.

pub const clock_skew_tolerance_ms: Int

How far apart node clocks may be. A stored timestamp up to this far in the future is accepted; one further ahead is rejected.

pub fn count(store: SessionStore) -> Int

Number of rows currently stored (including not-yet-swept expired rows). Crashes the calling process if an External backend fails.

pub fn create(store: SessionStore) -> Session

Create a new session with a generated ID.

Crashes the calling process if an External backend fails (see try_create_with_data).

pub fn create_with_data(
  store: SessionStore,
  data: dict.Dict(String, String),
) -> Session

Create a new session with a generated ID and initial data, in one write.

Crashes the calling process if an External backend fails (see try_create_with_data).

pub const default_absolute_ttl_ms: Int

Default absolute lifetime: 24 hours.

pub const default_idle_ttl_ms: Int

Default idle lifetime: 2 hours.

pub fn default_store_config() -> StoreConfig

Default store lifetimes (24h absolute, 2h idle, sweep every minute).

pub const default_sweep_interval_ms: Int

Default sweep interval: 60 seconds.

pub fn delete(store: SessionStore, id: String) -> Nil

Delete a session (logout). Bound WebSockets on every node are told to close. Crashes the calling process if an External backend fails (see try_delete).

pub fn ensure_registry() -> Nil

Start the live-connection registry scope, not linked to the caller, when it is not running.

brazier.start owns the scope in its supervision tree; call this only to bind sessions without it (application.start, tests, embedding). A supervised start fails when the scope already runs unsupervised.

pub fn fetch(
  store: SessionStore,
  id: String,
) -> Result(Session, SessionStoreError)

Get a live session by ID and refresh its idle deadline, returning why there is none.

pub fn generate_id() -> String

Generate a cryptographically random, URL-safe token.

Used for session IDs and session-bound CSRF tokens.

pub fn get(
  store: SessionStore,
  id: String,
) -> option.Option(Session)

Get a live session by ID and refresh its idle deadline.

Returns None for unknown, deleted, rotated, or expired sessions. Expired sessions are deleted on the spot and their bound sockets are notified. Crashes the calling process if an External backend fails (see fetch).

pub fn get_value(
  session: Session,
  key: String,
) -> option.Option(String)

Get a value from a session.

pub fn id_from_request(
  req: request.Request(body),
  cookie_name: String,
) -> Result(String, Nil)

Read the raw session id from a request cookie.

Does NOT check the store; use get to validate the session.

pub fn is_current_process_bound(
  store: SessionStore,
  session_id: String,
) -> Bool

Whether the calling process is bound to the session (by bind_current_process). A WebSocket re-check uses it to tell a socket that signed in from one that never did: once bound, the session going away must close the socket, never turn it into a guest.

pub fn new_store(name: String) -> SessionStore

Create a new LocalEts session store with default lifetimes.

The ETS table is owned by the calling process; the maintenance process (sweeper and re-checks) exits when that process exits.

pub fn new_store_with_config(
  name: String,
  config: StoreConfig,
) -> SessionStore

Create a new LocalEts session store with explicit lifetimes.

pub fn registry_running() -> Bool

Whether the registry scope is running on this node.

pub fn registry_supervised() -> supervision.ChildSpecification(
  Nil,
)

A child specification that starts the registry scope linked to a supervisor. The root supervision tree uses it. Starting fails when the scope already runs (for example after ensure_registry).

pub fn revocation_reason_to_string(
  reason: RevocationReason,
) -> String

Stable text for a revocation reason (sent to bound processes).

pub fn rotate(
  store: SessionStore,
  old_id: String,
) -> Result(Session, SessionStoreError)

Replace a session id with a fresh one carrying the same data.

The old id is deleted first-come-first-served: if two requests rotate the same id concurrently exactly one succeeds and the other gets SessionNotFound. Bound WebSockets on the old id are told to close; they reconnect with the new cookie.

pub fn secure_compare(a: String, b: String) -> Bool

Constant-time string comparison for secrets such as CSRF tokens.

pub fn set(
  store: SessionStore,
  session: Session,
  key: String,
  value: String,
) -> Result(Session, SessionStoreError)

Atomically set one key in a session.

The write is applied to the CURRENT stored data (not the possibly stale session argument), so concurrent set calls on different keys never lose each other’s writes. A deleted or expired session is never recreated: the error is returned instead.

pub fn start_store(
  backend: SessionBackend,
  config: StoreConfig,
) -> Result(SessionStore, SessionStoreError)

Create a session store on any backend. The calling process owns it: a LocalEts table and the maintenance process live as long as it does.

pub fn store_config(store: SessionStore) -> StoreConfig

The lifetime configuration of a store.

pub fn store_error_to_string(err: SessionStoreError) -> String

Stable text for a store error.

pub fn sweep_expired(
  store: SessionStore,
) -> Result(Int, SessionStoreError)

Delete every expired session and notify their bound sockets. Returns how many sessions were removed. Runs automatically every sweep_interval_ms; exposed for tests and manual maintenance.

pub fn try_create_with_data(
  store: SessionStore,
  data: dict.Dict(String, String),
) -> Result(Session, SessionStoreError)

Create a new session, returning backend failures.

pub fn try_delete(
  store: SessionStore,
  id: String,
) -> Result(Nil, SessionStoreError)

Delete a session (logout), returning backend failures. Bound WebSockets are told to close even when the backend fails, so a failed logout never leaves a live socket behind; the session itself may still exist then.

pub fn update(
  store: SessionStore,
  id: String,
  transform: fn(dict.Dict(String, String)) -> dict.Dict(
    String,
    String,
  ),
) -> Result(Session, SessionStoreError)

Atomically apply transform to a session’s data.

transform may run more than once if another writer wins the race, so it must be pure.

✨ Search Document