Skip to main content

README.md

# DurableStash

Durable, browser-session-scoped server state for Phoenix LiveView.

DurableStash is a [LiveStash](https://github.com/software-mansion-labs/live-stash)
adapter backed by [DurableServer](https://hex.pm/packages/durable_server): 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

```elixir
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:

```elixir
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

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

## Credits

DurableStash stands on two lineages:

- the adapter API of **[LiveStash](https://github.com/software-mansion-labs/live-stash)**
  by Software Mansion
- the durable-process runtime of **[DurableServer](https://github.com/phoenixframework/durable_server)**
  by Chris McCord