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
- Shared state per arbitrary key (room id, dashboard id, etc), across a cluster of nodes.
-
Automatic updates delivered as
@live_assignsto be used in templates. - Simple macros for declaring a state spec and LiveView clients
- ACL-based permission model (group vs public)
- TTL-based auto-shutdown when no clients remain
- Direct server API usable from non-LiveView processes
Installation
Add to mix.exs:
def deps do
[
{:phoenix_live_state, "~> 0.1.0"}
]
endAdd 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
...
endUsage 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
endUse 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
endServer 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/1ACL
Defines access control list (ACL) for the state server. Clients are considered:
-
group: if they called
StateServer.attach/1 - public: all other processes.
To each type of client, you may have the following permissions:
:read- can read the state of the server:write- can write into the state of the server:delete- can delete the state of the server:reset- can reset the state of the server
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
-
First
live_state/2starts a state server for the key (if absent). -
Clients attach; each gets full state as
@live_assigns. -
Writes (
live_assign/3or server API) broadcast diffs (new|updated|deleted|reset). - When last client detaches a TTL countdown begins (default 60s).
- 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.