This is still in alpha, I'm figuring out the right apis. Comments and ideas welcome.
PhoenixDatastar
A LiveView-like experience for Phoenix using Datastar's SSE + Signals architecture.
Build interactive Phoenix applications with Datastar's simplicity: SSE instead of WebSockets, hypermedia over JSON, and a focus on performance.
Installation
With Igniter
If you have Igniter installed, run:
mix igniter.install phoenix_datastar
This will automatically:
- Add the Registry to your supervision tree
- Enable stripping of debug annotations in dev
- Add the Datastar JavaScript to your layout
- Import the router macro
- Add
live_sseanddatastarhelpers to your web module
You'll then just need to add your routes (the installer will show you instructions).
Manual Installation
Add phoenix_datastar to your list of dependencies in mix.exs:
def deps do
[
{:phoenix_datastar, "~> 0.1.4"}
]
endThen follow the setup steps below.
1. Add Datastar to your layout
Include the Datastar JavaScript library in your layout's <head>:
<script
type="module"
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js"
></script>2. Add to your supervision tree
In your application.ex:
children = [
# ... other children
{Registry, keys: :unique, name: PhoenixDatastar.Registry},
# ... rest of your children
]3. Import the router macro
In your router:
import PhoenixDatastar.Router
scope "/", MyAppWeb do
pipe_through :browser
datastar "/counter", CounterStar
end4. Create :live_sse and :datastar in your _web.ex
defmodule MyAppWeb do
#... existing calls
def live_sse do
quote do
use PhoenixDatastar, :live
import PhoenixDatastar.Actions
unquote(html_helpers())
end
end
def datastar do
quote do
use PhoenixDatastar
import PhoenixDatastar.Actions
unquote(html_helpers())
end
end
end5. Strip debug annotations in dev (optional)
In your config/dev.exs, enable stripping of LiveView debug annotations from SSE patches:
config :phoenix_datastar, :strip_debug_annotations, trueThis removes <!-- @caller ... --> comments and data-phx-loc attributes from SSE patches. The initial page load keeps annotations intact for debugging.
6. Customize the mount template (optional)
PhoenixDatastar ships with a built-in mount template (PhoenixDatastar.DefaultHTML) that wraps your view content with the necessary Datastar signals and SSE initialization. You don't need to create your own — it works out of the box.
The default template automatically:
- Injects
session_idas a Datastar signal - Initializes all assigns from
mount/3as Datastar signals (via@initial_signals) - Sets up the SSE stream connection for live views
If you need to customize it (e.g., add classes, extra attributes, or additional markup), create your own module:
defmodule MyAppWeb.DatastarHTML do
use Phoenix.Component
def mount(assigns) do
~H"""
<div
id="app"
class="my-wrapper"
data-signals={Jason.encode!(Map.put(@initial_signals, :session_id, @session_id))}
data-init__once={@stream_path && "@get('#{@stream_path}', {openWhenHidden: true})"}
>
{@inner_html}
</div>
"""
end
endAvailable assigns in the mount template:
@session_id— unique session identifier@initial_signals— map of user-defined assigns frommount/3(internal assigns likebase_pathare filtered out)@stream_path— SSE stream URL (nil for stateless views)@event_path— event POST URL@inner_html— the rendered view content
Then configure it in config/config.exs:
config :phoenix_datastar, :html_module, MyAppWeb.DatastarHTMLOr per-route:
datastar "/custom", CustomStar, html_module: MyAppWeb.DatastarHTMLUsage
Basic Example
defmodule MyAppWeb.CounterStar do
use MyAppWeb, :live_sse
# if not present in `my_app_web.ex` file use
# `use PhoenixDatastar, :live`
@impl PhoenixDatastar
def mount(_params, _session, socket) do
# Assigns are automatically initialized as Datastar signals —
# no need to manually add data-signals in your template
{:ok, assign(socket, count: 0)}
end
@impl PhoenixDatastar
def handle_event("increment", _payload, socket) do
{:noreply,
socket
|> update(:count, &(&1 + 1))
|> patch_elements("#count", &render_count/1)}
end
@impl PhoenixDatastar
def render(assigns) do
~H"""
<div>
Count: <span id="count">{@count}</span>
<button data-on:click={event("increment")}>+</button>
</div>
"""
end
defp render_count(assigns) do
~H|<span id="count">{@count}</span>|
end
endNote: In previous versions you had to manually declare Datastar signals in your template with
data-signals={Jason.encode!(%{count: @count})}. This is no longer needed — assigns set inmount/3are automatically injected as signals by the wrapper template.
The Lifecycle
PhoenixDatastar uses a hybrid of request/response and streaming:
- Initial Page Load (HTTP):
GET /countercallsmount/3andrender/1, returns full HTML - SSE Connection:
GET /counter/streamopens a persistent connection, starts a GenServer (live views only) - User Interactions:
POST /counter/_event/:eventtriggershandle_event/3, updates pushed via SSE (live) or returned directly (stateless)
Callbacks
| Callback | Purpose |
|---|---|
mount/3 | Initialize state on page load |
handle_event/3 | React to user actions |
handle_info/2 | Handle PubSub messages, timers, etc. |
render/1 | Render the full component |
terminate/1 | Cleanup on disconnect (optional) |
Socket API
# Assign values
socket = assign(socket, :count, 0)
socket = assign(socket, count: 0, name: "test")
# Update with a function
socket = update(socket, :count, &(&1 + 1))
# Queue a DOM patch (sent via SSE)
socket = patch_elements(socket, "#selector", &render_fn/1)
socket = patch_elements(socket, "#selector", ~H|<span>html</span>|)Action Macros
PhoenixDatastar provides macros to simplify generating Datastar action expressions in your templates.
Requirements
assigns.session_idmust be set in your template context (automatically set by PhoenixDatastar)assigns.event_pathmust be set (automatically set by PhoenixDatastar)- A
<meta name="csrf-token">tag must be present in your layout (Phoenix includes this by default)
event/2
Generates a Datastar @post action for triggering server events.
# Simple event
<button data-on:click={event("increment")}>+1</button>
# Event with options
<button data-on:click={event("toggle_code", "name: 'counter'")}>Toggle</button>
# With signals
<button data-on:click={event("update", "value: $count")}>Update</button>Stateless vs Live Views
# Stateless view - no persistent connection, events handled synchronously
use MyAppWeb, :datastar
# or: use PhoenixDatastar
# Live view - persistent SSE connection with GenServer state
use MyAppWeb, :live_sse
# or: use PhoenixDatastar, :liveStateless views handle events synchronously - state is restored from client signals on each request, and the response is returned immediately. No GenServer or SSE connection is maintained.
Live views maintain a GenServer and SSE connection. Use :live when you need:
- Real-time updates from the server (PubSub, timers)
- Persistent server-side state across interactions
handle_info/2callbacks
Links
- Datastar - The frontend library this integrates with
- Phoenix LiveView - The inspiration for the callback design
License
MIT License - see LICENSE for details.