Continuing from the PhoenixDatastar thread — that package is deprecated in favor of Dstar.
Dstar is a batteries-included Datastar toolkit for Elixir: SSE primitives, signal/element patching, event dispatch, and — since 0.1 — a unified page model. A Datastar page is one module and one router line, with LiveView-familiar ergonomics over plain HTTP + SSE:
defmodule MyAppWeb.CounterPage do
use Dstar.Page
def mount(conn, _params), do: assign(conn, count: 0)
def handle_event(conn, "increment", signals) do
patch_signals(conn, %{count: (signals["count"] || 0) + 1})
end
def render(assigns) do
~H"""
<div data-signals:count={@count}>
<h1 data-text="$count"></h1>
<button data-on:click={event("increment")}>+1</button>
</div>
"""
end
end
Live updates are just as small — declare how to subscribe, and the library owns the receive loop (liveness checks, stray-message tolerance, cleanup on disconnect):
# Same page module — push PubSub broadcasts to the browser:
def handle_connect(conn, _params) do
MyAppWeb.Endpoint.subscribe("ticker")
conn
end
def handle_info(%Phoenix.Socket.Broadcast{payload: p}, conn) do
patch_signals(conn, %{tick: p.count})
end
The HTML to initialize the stream and handle the updated signals:
<div data-init={connect()} data-on:online__window={connect()}>
<span data-text="$tick"></span>
</div>
The router:
# router.ex
import Dstar.Router
scope "/", MyAppWeb do
pipe_through :browser
dstar "/counter", CounterPage
end
Also included:
mix dstar.https— one-command trusted HTTPS for local development (hosts entry + mkcert cert). Browsers cap HTTP/1.1 at 6 connections per domain and SSE streams eat them until the app silently stalls; HTTP/2 fixes it but needs TLS, and this removes the setup slog. Grew out of collaboration with @Neophen’s StarView- Per-tab stream deduplication, shared components with colocated handlers, router macros, and Dstar.Test for asserting on SSE patches.
- A small functional core that needs only plug + jason — the page layer and its phoenix/phoenix_live_view deps are optional.
The page layer is in alpha (0.1.0-alpha.2) until it survives a full production-app migration; the functional core is the same battle-tested code from the 0.0.x line. All 0.0.x code runs unchanged.
Why Datastar?
Both LiveView and Datastar keep the source of truth on the server — the difference is what holds it. LiveView mirrors your state into a long-lived process per visitor over a websocket; that process holds memory, must re-join and rehydrate on every reconnect, and ties UI lifetime to socket lifetime. Datastar skips the middleman: the backend is the state, and it drives the browser directly by patching HTML and signals over SSE. A GET renders the page, events are ordinary POSTs, reconnection is the browser’s job, and every request is a regular Plug.Conn. Signals exist only for ephemeral UI bits (toggles, form inputs) — everything real stays in your domain. You send fat chunks of morphed, Brotli-compressed HTML rather than managing fine-grained diffs, and the wire format is simple enough to read with curl.
| LiveView | Datastar + Dstar | |
|---|---|---|
| Transport | WebSocket | Plain HTTP + SSE |
| Server memory per visitor | Long-lived process + assigns | None between requests |
| Reconnects / deploys | Socket re-join, state rehydration | Browser retries SSE; nothing to rehydrate |
| Updates over the wire | Fine-grained template diffs | Fat-morphed HTML + compression (Brotli ~200:1 on streams) |
| Events | Socket messages | Regular POSTs (curl-able, CSRF like any form) |
| Programming model | mount/handle_event/handle_info on a socket |
Same shape, on a Plug.Conn |
| Auth flow | Plug pipeline + on_mount hooks (socket re-auths separately) |
Plug pipeline only — every request passes through it |
Feedback very welcome — especially on the Dstar.Page API surface, and from anyone on Windows who tries dstar.https (currently prints manual instructions).
What’s next?
Currently Dstar pages come in two flavors: plain “DeadView” (render + events, no stream) and streaming (define handle_connect/2 and the library runs a long-lived SSE loop for pushing PubSub broadcasts). I’m exploring an explicit third tier: :stateful — streaming plus an opt-in per-tab GenServer that holds assigns decoupled from the connection. LiveView-style ergonomics, except the state survives reconnects and the SSE drop/retry dance by construction. Very much thinking out loud at this stage — feedback on the shape welcome.






















