From elixir-dev
Provides Phoenix LiveView best practices: no DB queries in mount (called twice), load data in handle_params, security scopes, scoped PubSub topics, GenServer polling, async assigns, and gotchas.
npx claudepluginhub gsmlg-dev/code-agent --plugin elixir-devThis skill uses the workspace's default tool permissions.
Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.
NO DATABASE QUERIES IN MOUNT
mount/3 is called TWICE (HTTP request + WebSocket connection). Queries in mount = duplicate queries.
def mount(_params, _session, socket) do
# NO database queries here! Called twice.
{:ok, assign(socket, posts: [], loading: true)}
end
def handle_params(params, _uri, socket) do
# Database queries here - once per navigation
posts = Blog.list_posts(socket.assigns.scope)
{:noreply, assign(socket, posts: posts, loading: false)}
end
mount/3 = setup only (empty assigns, subscriptions, defaults) handle_params/3 = data loading (all database queries, URL-driven state)
No exceptions: Don't query "just this one small thing" in mount. Don't "optimize later". LiveView lifecycle is non-negotiable.
Scopes address OWASP #1 vulnerability: Broken Access Control. Authorization context is threaded automatically—no more forgetting to scope queries.
def list_posts(%Scope{user: user}) do
Post |> where(user_id: ^user.id) |> Repo.all()
end
def subscribe(%Scope{organization: org}) do
Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end
Unscoped topics = data leaks between tenants.
Bad: Every connected user makes API calls (multiplied by users). Good: Single GenServer polls, broadcasts to all via PubSub.
Use assign_async/3 for data that can load after mount:
def mount(_params, _session, socket) do
{:ok, assign_async(socket, :user, fn -> {:ok, %{user: fetch_user()}} end)}
end
terminate/2 only fires if you're trapping exits—which you shouldn't do in LiveView.
Fix: Use a separate GenServer that monitors the LiveView process via Process.monitor/1, then handle :DOWN messages to run cleanup.
Calling start_async with the same name while a task is in-flight: the later one wins, the previous task's result is ignored.
Fix: Call cancel_async/3 first if you want to abort the previous task.
The socket in handle_out intercept is a snapshot from subscription time, not current state.
Why: Socket is copied into fastlane lookup at subscription time for performance.
Fix: Use separate topics per role, or fetch current state explicitly.
When merging classes on components, precedence is determined by stylesheet order, not HTML order. If btn-primary appears later in the compiled CSS than bg-red-500, it wins regardless of HTML order.
Fix: Use variant props instead of class merging.
The :content_type in %Plug.Upload{} is user-provided. Always validate actual file contents (magic bytes) and rewrite filename/extension.
To verify webhook signatures, you need the raw body. But Plug.Parsers consumes it.
{:ok, body, conn} = Plug.Conn.read_body(conn)
verify_signature!(conn, body)
%{conn | body_params: JSON.decode!(body)}
Don't use preserve_req_body: true—it keeps the entire body in memory for ALL requests.
%Plug.Upload{}.content_type for securityAny of these? Re-read The Iron Law and the Gotchas section.