Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Use this skill before writing ANY LiveView module or .heex template.
@impl true before every callback (mount, handle_event, handle_info, render)connected?(socket) before PubSub subscriptions, timers, or side effectsMap.get(assigns, :key, default) for optional assigns in helper functions{:ok, socket} from mount, {:noreply, socket} from handle_eventwith for error handling in event handlers — assign errors to socket, don't crashauto_upload: true with form submission — use manual uploads insteadcore_components.ex for existing components before creating custom onesliveview-streams skill for detailsmount/3 — initialize all assigns with static defaultshandle_params/3 — set URL-dependent assigns, subscribe to PubSubrender/1 — reference only assigns initialized in steps 1–2handle_event/3 — implement user interactions with proper error handlingKeyError before WebSocket connectsLiveView renders twice per page load:
connected?(socket) is false; side effects won't work.connected?(socket) is true; events and live updates work.Both phases run mount → handle_params → render. Initialize all assigns to safe defaults in Phase 1 so the static HTML never raises a KeyError.
@impl true
def mount(_params, _session, socket) do
# Initialize static defaults here; URL-dependent assigns go in handle_params
socket =
socket
|> assign(:user, nil)
|> assign(:loading, false)
|> assign(:data, [])
# Only subscribe when connected — avoids double subscriptions across both render phases
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
end
{:ok, socket}
endDefer expensive operations to the connected phase:
@impl true
def mount(_params, _session, socket) do
socket =
if connected?(socket) do
assign(socket, :data, run_expensive_query())
else
assign(socket, :data, []) # Placeholder for static render
end
{:ok, socket}
end✅ Validation checkpoint: Verify all assigns used in render/1 are initialized — the static render must display without a KeyError.
@impl true
def handle_event("delete", %{"id" => id}, socket) do
case Posts.delete_post(id) do
{:ok, _post} ->
{:noreply, assign(socket, :posts, Posts.list_posts())}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Could not delete post")}
end
endFor create/update events, use the Error Handling pattern below — assign changeset errors to the socket rather than raising.
✅ Validation checkpoint: Each handler must return {:noreply, socket}; error paths assign errors to the socket rather than raising.
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, update(socket, :posts, fn posts -> [post | posts] end)}
end
@impl true
def handle_info(%{event: "presence_diff"}, socket) do
{:noreply, assign(socket, :online_users, get_presence_count())}
endCalled in BOTH render phases on URL changes. Place URL-dependent assigns here so they are available in both static and connected renders.
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
post = Posts.get_post!(id)
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
end
{:noreply, assign(socket, :post, post)}
end
@impl true
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end# Single assign
socket = assign(socket, :count, 0)
# Multiple assigns
socket = assign(socket, count: 0, name: "User", active: true)
# Update existing assign
socket = update(socket, :count, &(&1 + 1))In render/1 — direct access is safe when initialized in mount:
@impl true
def render(assigns) do
~H"""
<p>Count: <%= @count %></p>
"""
endIn helper functions — use Map.get for optional assigns:
defp format_user(socket) do
case Map.get(socket.assigns, :current_user) do
nil -> "Guest"
user -> user.name
end
end# Full page reload (new LiveView)
{:noreply, push_navigate(socket, to: ~p"/users")}
# Patch (same LiveView, different params)
{:noreply, push_patch(socket, to: ~p"/posts/#{post}")}def card(assigns) do
~H"""
<div class="card">
<h3><%= @title %></h3>
<p><%= @content %></p>
</div>
"""
end
# Usage in template
# <.card title="Hello" content="World" /><.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:title]} label="Title" />
<.input field={@form[:body]} type="textarea" label="Body" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>@impl true
def mount(_params, _session, socket) do
changeset = Post.changeset(%Post{}, %{})
{:ok, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("validate", %{"post" => params}, socket) do
changeset =
%Post{}
|> Post.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end@impl true
def handle_event("save", %{"post" => post_params}, socket) do
case Posts.create_post(post_params) do
{:ok, post} ->
socket =
socket
|> put_flash(:info, "Created!")
|> assign(:post, post)
{:noreply, socket}
{:error, %Ecto.Changeset{} = changeset} ->
socket =
socket
|> put_flash(:error, "Please correct the errors")
|> assign(:changeset, changeset)
{:noreply, socket}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "An error occurred: #{reason}")}
end
endSee ../../../agents/liveview-checklist.md for a step-by-step LiveView development checklist. Related skills: liveview-streams, phoenix-pubsub-patterns, phoenix-liveview-auth, phoenix-scopes, testing-essentials.
phoenix-channels-essentials insteadliveview-streams instead for DOM efficiencyphoenix-uploads insteadphoenix-liveview-auth and phoenix-scopes instead