Storage contract for Keyfob requests — a small keyed store with TTLs and two atomicity guarantees the login flow leans on:
update/2applies its function serially per key (no lost updates between a racing approve and expiry sweep);take/1removes-and-returns atomically (the single-use gate for login tokens: of two racingconsumecalls, exactly one wins).
Keyfob.Store.ETS is the built-in single-node implementation. For a
cluster, implement this behaviour over storage all nodes share (your
database, Redis) and set config :keyfob, store: MyApp.KeyfobStore.
Values must be treated as opaque. Expired entries must behave as
missing (:error) even if not yet swept.
Summary
Callbacks
Removes a value. Idempotent.
Fetches a live (non-expired) value.
Stores value under key for ttl_ms milliseconds.
Atomically removes and returns a live value.
Atomically applies fun to the live value under key.
Types
Callbacks
@callback delete(key()) :: :ok
Removes a value. Idempotent.
Fetches a live (non-expired) value.
@callback put(key(), value(), ttl_ms :: pos_integer()) :: :ok
Stores value under key for ttl_ms milliseconds.
Atomically removes and returns a live value.
@callback update(key(), (value() -> {:ok, value()} | {:error, term()})) :: {:ok, value()} | {:error, term()} | :error
Atomically applies fun to the live value under key.
fun returns {:ok, new_value} to store (keeping the remaining TTL)
or {:error, reason} to leave the value untouched. Returns the fun's
result, or :error when the key is missing/expired.