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 (unlike the stock ETS adapter, recovery happens on every mount, not just reconnects)
  • WebSocket reconnects (Wi-Fi hiccups)
  • LiveView crashes
  • redeploys — state lives in object storage, not BEAM memory

State dies with the browser session: cleared cookies, another browser, or TTL expiry mean defaults.

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

  def handle_event("increment", _params, socket) do
    socket = update(socket, :count, &(&1 + 1))
    {:noreply, LiveStash.stash(socket)}
  end
end

Scopes

Not all state wants the same recovery policy, so each stored key declares one:

use LiveStash, adapter: DurableStash,
  stored_keys: [
    theme: :session,    # recover on every mount (the default for bare atoms)
    draft: :reconnect   # recover only on rejoins; cleared on fresh navigation
  ]
  • :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.

See the DurableStash moduledoc for setup (adapter registration, backend config, the ensure_session_id plug) and all options (:vsn, :migrate, :secret).

How it works

  • A plug puts a random "sid" into the cookie session; the adapter hashes it (with a secret) into the storage key of one DurableStash.Session process.
  • All LiveViews of a session write through that single actor: per-key diffs, key-wise merge, per-key last-write-wins. Two tabs writing different keys cannot clobber each other; same-key writes are totally ordered.
  • Every accepted write syncs to object storage immediately, so a deploy can never lose acknowledged state.
  • Values are JSON-normalized at stash time — what you recover in dev is exactly what you would recover after a redeploy in prod.

DurableStash.TestBackend ships with the package: a faithful in-memory DurableServer.StorageBackend (including etag CAS) for tests and make run style development without S3 credentials.

Installation

def deps do
  [
    {:durable_stash, "~> 0.1.0"}
  ]
end

Credits

DurableStash stands on two lineages: