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).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
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 — manual uploads are required to control when files are consumed relative to form datacore_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 connectsBoth phases run mount → handle_params → render. Initialize all assigns to safe defaults in Phase 1 (connected?(socket) is false) so the static HTML never raises a KeyError. Side effects, PubSub, and timers only work in Phase 2 (connected?(socket) is true).
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:user, nil)
|> assign(:loading, false)
|> assign(:data, [])
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, [])
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.
Match on the message shape and update assigns accordingly:
@impl true
def handle_info({:post_updated, post}, socket) do
{:noreply, assign(socket, :post, post)}
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}
endAssigns drive rendering. Use assign/3 for direct values and update/3 when the new value depends on the current one. In render/1, direct @key access is safe when the assign is initialized in mount; in helper functions, guard optional assigns with Map.get/3 to avoid KeyError.
Single assign:
socket = assign(socket, :count, 0)Multiple assigns at once:
socket =
socket
|> assign(:user, user)
|> assign(:loading, false)
|> assign(:posts, [])Incremental change — depend on the current value:
❌ Bad — reads the assign manually, then re-assigns:
count = socket.assigns.count
assign(socket, :count, count + 1)✅ Good — update/3 transforms the current value:
update(socket, :count, fn count -> count + 1 end)Safe helper access:
❌ Bad — a maybe-missing @current_user raises KeyError:
defp format_user(socket) do
socket.assigns.current_user.name
end✅ Good — guard optional assigns with Map.get/3:
defp format_user(socket) do
case Map.get(socket.assigns, :current_user, nil) 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}")}<.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
endRelated skills: liveview-streams, phoenix-pubsub-patterns, phoenix-liveview-auth, phoenix-scopes, testing-essentials.
.heex template involvedliveview-streams instead for DOM efficiencyphoenix-liveview-auth and phoenix-scopes insteadDrive the full lifecycle with Phoenix.LiveViewTest — mount, render, events, and navigation. See assets/liveview_test_template.md for a LiveView test template and assets/component_test_template.md for a function-component test template.
| ❌ Don't | ✅ Do |
|---|---|
Access an assign in render/1 that was never initialized | Initialize every assign in mount/3 or handle_params/3 before it is rendered |
| Subscribe to PubSub or start timers in the static mount | Guard side effects with if connected?(socket) |
| Query the database directly from the LiveView | Call context functions (e.g. Posts.list_posts/0) instead |
| Read an assign then re-assign it to increment | Use update/3 to transform the current value |
Use @key for an optional assign in a helper | Use Map.get(assigns, :key, default) to avoid KeyError |
| Return a bare socket or the wrong tuple from a callback | Return {:ok, socket} from mount, {:noreply, socket} from handlers |
Omit @impl true on a callback | Add @impl true before mount, handle_event, handle_info, handle_params |
| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | phoenix-liveview-essentials | liveview-streams |
| ecto-changeset-patterns | phoenix-liveview-essentials | phoenix-liveview-auth |
Companion skills: liveview-streams, phoenix-scopes, phoenix-pubsub-patterns, testing-essentials.
.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators