PhoenixLiveState

PhoenixLiveState enables LiveView processes to share reactive state across a cluster of nodes. Attach LiveViews to a state server and get automatic assign synchronization.

Features

Installation

Add to mix.exs:

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

Add the supervisor in your application tree, BEFORE your endpoint supervisor!

defmodule MyApp.Application do
  ...
  def start(_type, _args) do
    children = [
      ...,
      PhoenixLiveState.Supervisor,
      MyApp.Endpoint
    ]
    ...
  end
  ...
end

Usage Example

Define a State Spec

The state spec defines the initial state, permissions and any other settings for your shared state. This function is called when the state server is started and when it's reset.

defmodule MyAppWeb.ChatRoomState do
  use PhoenixLiveState, :state

  @impl true
  def on_mount(room_id) do
    {:ok,
      initial_state: [messages: []]
    }
  end
end

Use in a LiveView

In you LiveView, you use PhoenixLiveState, client: opts to inject the hooks as well as the helper functions: live_state/3 and live_assign/2

On the mount/3 function, call live_state/3 to attach your liveview to the state server. From there, it will inject a new assign, @live_assigns in your socket.

That's it. Now, whenever the state server is updated, all connected LiveViews will receive the updated state!

defmodule MyAppWeb.ChatRoomLive do
  use MyAppWeb, :live_view
  use PhoenixLiveState, client: [
    live_state: MyAppWeb.ChatRoomState
  ]

  def mount(%{"id" => room_id}, _session, socket) do
    {:ok, live_state(socket, room_id)}
  end

  def handle_event("submit", %{"message" => new_msg}, socket) do
    msgs = socket.assigns.live_assigns.messages
    {:noreply, socket |> live_assign(:messages, [new_msg, msgs]) |> assign(...)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <div :for={message <- @live_assigns.messages}>
        {message}
      </div>
    </div>
    <div>
      <.form :for={%{}} phx-submit="submit">
        <.input name="message" value="">
      </.form>
    </div>
    """
  end
end

Server API

You may also interact with the shared state server directly, as long as you know the server's key. This is useful to integrate with non-liveview processes, like a Phoenix Controller:

PhoenixLiveState.StateServer.get(key)              # {:ok, full_state}
PhoenixLiveState.StateServer.get(key, :messages)   # {:ok, value} | {:error, :property_not_found}
PhoenixLiveState.StateServer.put(key, :users, %{})
PhoenixLiveState.StateServer.delete(key, :typing)
PhoenixLiveState.StateServer.reset(key)            # reruns on_mount/1

ACL

Defines access control list (ACL) for the state server. Clients are considered:

To each type of client, you may have the following permissions:

You may use th alias :all to indicate that the client type has full access to the server.

Examples

%{
  group: :all,    # attached clients
  public: [:read] # other callers
}

Lifecycle

  1. First live_state/2 starts a state server for the key (if absent).
  2. Clients attach; each gets full state as @live_assigns.
  3. Writes (live_assign/3 or server API) broadcast diffs (new|updated|deleted|reset).
  4. When last client detaches a TTL countdown begins (default 60s).
  5. Server exits if no new client attaches before TTL expires.

License

MIT © 2025 Marcos da Silva Ramos

Changelog

See CHANGELOG.md.


Minimal, experimental API—pin versions in production.