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
stream_insert/3 and stream_delete/3 for incremental updates — never replace the entire stream assignphx-update="stream" in templates — required for stream DOM patching; missing this causes full re-rendersreset: true for filtering and sorting — clears existing items before inserting the new result setstream_configure/3 before stream/3 — configuring a custom dom_id after the stream is initialized has no effectFollow this sequence when implementing LiveView streams:
id attribute format (e.g., post-#{post.id})stream_configure/3 in mount/3 to set custom DOM ID generatorstream(socket, :name, collection) in mount/3phx-update="stream" container and id={dom_id} on itemsstream_insert/stream_delete for all mutationsdefmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> stream(:posts, Blog.list_posts())}
end
@impl true
def handle_event("create", %{"post" => params}, socket) do
{:ok, post} = Blog.create_post(params)
{:noreply, stream_insert(socket, :posts, post, at: 0)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(id)
{:ok, _} = Blog.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
end
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
end
end<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id} class="post">
<h3><%= post.title %></h3>
<p><%= post.body %></p>
<button phx-click="delete" phx-value-id={post.id}>
Delete
</button>
</div>
</div>@per_page 20
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page, 1)
|> assign(:loading, false)
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> stream(:posts, Blog.list_posts(page: 1, per_page: @per_page))}
end
@impl true
def handle_event("load_more", _params, socket) do
page = socket.assigns.page + 1
new_posts = Blog.list_posts(page: page, per_page: @per_page)
{:noreply,
socket
|> assign(:page, page)
|> then(fn s -> Enum.reduce(new_posts, s, &stream_insert(&2, :posts, &1)) end)}
end<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= post.title %>
</div>
</div>
<div phx-hook="InfiniteScroll" data-page={@page}>
<button phx-click="load_more" disabled={@loading}>Load More</button>
</div>@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "posts")
end
{:ok,
socket
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> stream(:posts, Blog.list_posts())}
end
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
endBroadcast from your context:
def create_post(attrs) do
{:ok, post} = Repo.insert(Post.changeset(%Post{}, attrs))
Phoenix.PubSub.broadcast(MyApp.PubSub, "posts", {:post_created, post})
{:ok, post}
endUse reset: true to clear existing items before inserting new ones — required for filtering and sorting:
# Filtering
def handle_event("filter", %{"status" => status}, socket) do
{:noreply, stream(socket, :posts, Blog.list_posts_by_status(status), reset: true)}
end
# Sorting — re-fetch from the database because streams do not store items in assigns
def handle_event("sort", %{"column" => col, "direction" => dir}, socket) do
direction = String.to_existing_atom(dir)
sorted = Blog.list_posts(order_by: [{direction, String.to_existing_atom(col)}])
{:noreply,
socket
|> assign(:sort_direction, direction)
|> stream(:posts, sorted, reset: true)}
enddef handle_event("save_edit", %{"id" => id, "post" => params}, socket) do
post = Blog.get_post!(id)
{:ok, updated_post} = Blog.update_post(post, params)
{:noreply,
socket
|> assign(:editing_id, nil)
|> stream_insert(:posts, updated_post)}
end<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= if @editing_id == post.id do %>
<.form phx-submit="save_edit">
<input name="post[title]" value={post.title} />
<button type="submit">Save</button>
<button type="button" phx-click="cancel_edit">Cancel</button>
</.form>
<% else %>
<h3><%= post.title %></h3>
<button phx-click="start_edit" phx-value-id={post.id}>Edit</button>
<% end %>
</div>
</div>If stream patching is not working as expected, check for:
phx-update="stream" on the container <div>id={dom_id} on each item elementstream_insert/stream_deletestream_insert without proper dom_id configurationstream_configure before stream when using custom IDsManual verification step: Open your browser's DevTools panel → Network tab → WS frames. Confirm incoming frames contain targeted patch operations for individual items, not full list replacements. If you see full re-renders, the first two checklist items above are the most common cause.
| ❌ Don't | ✅ Do |
|---|---|
| Replace the whole stream assign to update it | Use stream_insert/3 and stream_delete/3 for incremental changes |
Omit phx-update="stream" on the container | Add phx-update="stream" so LiveView patches items instead of re-rendering |
Forget id={dom_id} on each item element | Set id={dom_id} so DOM patching can target individual rows |
| Reuse non-unique IDs across pages in infinite scroll | Configure a stable dom_id (e.g. &"post-#{&1.id}") so IDs never collide |
| Sort or filter from items held in assigns | Re-fetch from the database and apply stream(..., reset: true) |
| Stream tiny collections (a handful of rows) | Use regular assigns; reserve streams for 100+ item lists |
Call stream_configure/3 after stream/3 | Configure the custom dom_id before the stream is initialized |
| Predecessor | This Skill | Successor |
|---|---|---|
| phoenix-liveview-essentials | liveview-streams | phoenix-pubsub-patterns |
| ecto-essentials | liveview-streams | testing-essentials |
Companion skills: phoenix-liveview-essentials, 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