Durable, browser-session-scoped server state for Phoenix LiveView.
DurableStash is a LiveStash adapter
backed by DurableServer: one
durable process per browser session, persisted to S3-compatible object
storage, shared by every LiveView of that session. State survives live
navigation, WebSocket reconnects, LiveView crashes, and redeploys — and
dies with the browser session.
Usage
defmodule MyAppWeb.SomeLive do
use MyAppWeb, :live_view
use LiveStash, adapter: DurableStash, stored_keys: [:count, :username]
def mount(_params, _session, socket) do
socket = assign(socket, count: 0, username: nil)
{_status, socket} = LiveStash.recover_state(socket)
{:ok, socket}
end
endCall LiveStash.recover_state/1 in mount/3 after assigning defaults
(recovered values overwrite them), and LiveStash.stash/1 whenever a
stored assign changes — or pass auto_stash: true.
Unlike the stock ETS adapter, DurableStash recovers on every mount —
fresh navigations included — and never deletes the stash on mount. State is
keyed by the browser session, not by the socket.
Setup
Register the adapter and configure the backend:
config :live_stash, adapters: [DurableStash] config :durable_stash, backend: {DurableServer.Backends.ObjectStore, bucket: "...", ...}, prefix: "durable_stash/", secret: "some-stable-secret"With that config,
LiveStash.Applicationstarts the DurableServer supervisor automatically throughchild_spec/1. Alternatively, run your ownDurableServer.Supervisorand point the adapter at it with the:supervisoroption.Put a session id into the cookie session, in the
:browserpipeline afterplug :fetch_session:plug :ensure_session_id defp ensure_session_id(conn, _opts) do if get_session(conn, "sid") do conn else sid = 16 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false) put_session(conn, "sid", sid) end end
Scopes
Not all state wants the same recovery policy. Each stored key declares a scope:
use LiveStash, adapter: DurableStash,
stored_keys: [
theme: :session, # recover on every mount (the default)
draft: :reconnect # recover only on reconnects; cleared on fresh mounts
]:session— recovered on every mount: live navigation, reconnects, crashes, redeploys. Right for settings the user expects to stick.:reconnect— recovered only when the client rejoins an existing view (_mounts > 0): Wi-Fi drops, LiveView crashes, and redeploys — the browser stays on the page through all of these. A fresh navigation to the view clears the stored values, so starting a "new thing" starts blank. Right for in-progress form drafts.
Options (via use LiveStash, adapter: DurableStash, ...)
:stored_keys(required) — assigns to persist. Bare atoms mean:sessionscope; see Scopes above for:reconnect.:permanentis reserved and raises for now.:vsn(default1) — version of this view's stored shape. On recovery, a stored slice with a different vsn is discarded to defaults unless:migrateis given.:migrate— 2-arity function(old_vsn, data) :: datareceiving the stored string-keyed data map and returning the migrated one. The migrated set is written back under the new vsn.:supervisor— DurableServer supervisor name (defaultconfig :durable_stash, :supervisor_name, falling back toDurableStash.Supervisor).:secret— mixed into the storage-key hash (defaultconfig :durable_stash, :secret).:session_id_key— cookie-session key holding the session id (default"sid").
What's storable
JSON-safe values only: no structs, tuples, pids, or functions; maps come
back with string keys. Offending values are skipped with a logged error, or
raise when config :durable_stash, on_invalid_value: :raise is set
(recommended for dev and test). Values are normalized through a JSON
round-trip at stash time, so what you recover in dev is byte-for-byte what
you'd recover after a redeploy in prod.
Summary
Functions
Starts the DurableServer supervisor from :durable_stash config when a
:backend is configured; otherwise starts an empty, harmless supervisor.
Invoked automatically by LiveStash.Application for registered adapters.