Server-Driven UI for Phoenix LiveView applications backed by Ash resources.
AshSDUI lets you define UI layouts as data — either in code or persisted in your database — and render them dynamically in LiveView without redeploying.
Key modules
AshSDUI.Component— macro for declaring and registering SDUI componentsAshSDUI.Registry— ETS-backed registry of all discovered componentsAshSDUI.Layout— unified API for registered and stored layout treesAshSDUI.UINode— built-in ETS resource for persisted layout nodesAshSDUI.Renderer— builds aTreeNodetree from a layout name orUINoderecordsAshSDUI.Cache— ETS-backed cache with automatic invalidation onUINodechangesAshSDUI.Notifier— Ash notifier that evicts cache entries on resource changesAshSDUI.Calculations.ResolveSubject— resolves{subject_resource, subject_id}to a live recordAshSDUI.Components.SDUIRoot— Phoenix component that recursively renders a treeAshSDUI.View— resolves Ash resources and SDUI metadata into generic view specsAshSDUI.Context— runtime actor/audience/tenant/device context for view resolutionAshSDUI.LayoutRecipe— app-extensible conversion from view specs to layout trees
Usage in a LiveView
Add use AshSDUI with a lookup strategy and call <.sdui_root /> in your template:
defmodule MyAppWeb.Live.PlayerDashboard do
use MyAppWeb, :live_view
use AshSDUI, lookup: {:from_params, :name}
def render(assigns) do
~H"""
<%= if @__sdui_tree__ do %>
<.sdui_root />
<% else %>
<div>Layout not found</div>
<% end %>
"""
end
endYou must reference @__sdui_tree__ in your template so Phoenix includes it in the
assigns passed to sdui_root. The injected mount/3 is declared defoverridable
— you can override it to add your own socket assigns.
Lookup strategies
{:from_params, :name}— reads the layout name from the socket params map{:static, "layout-name"}— always renders the named layout
Pass source:, status:, or node_resource: to use AshSDUI when a LiveView
should render a stored layout from a specific source or compatible node resource.
Defining a component
defmodule MyAppWeb.Components.Player.ScoreCard do
use MyAppWeb, :live_component
use AshSDUI.Component, fragment: """
fragment PlayerScoreCardData on Player {
displayName
currentScore
}
"""
def render(assigns) do
~H"""
<div>
<h2><%= @subject.display_name %></h2>
<p>Score: <%= @subject.current_score %></p>
</div>
"""
end
endComponents are registered automatically under a name derived from the module
(e.g., "Player.ScoreCard@v1"). Set @version "v2" before use AshSDUI.Component
to override the default version suffix.
See the README for full usage, layout definitions, UINode actions, and caching details.