plugins/elixir/skills/phoenix-thinking/SKILL.md
This skill should be used when the user asks to "add a LiveView page", "create a form", "handle real-time updates", "broadcast changes to users", "add a new route", "create an API endpoint", "fix this LiveView bug", "why is mount called twice?", or mentions handle_event, handle_info, handle_params, mount, channels, controllers, components, assigns, sockets, or PubSub. Covers where to load data (mount vs handle_params) and the LiveView lifecycle.
npx skillsauth add georgeguimaraes/claude-code-elixir phoenix-thinkingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.
Default: load data in mount/3.
def mount(_params, _session, socket) do
posts = Blog.list_posts(socket.assigns.current_scope)
{:ok, assign(socket, posts: posts)}
end
Yes, mount runs twice on initial load (HTTP dead render + WebSocket connect). So does handle_params/3. That's the LiveView lifecycle, not a bug to route around. Moving queries from mount to handle_params does not dedupe them.
Use handle_params/3 for data that changes on live navigation (push_patch / <.link patch={...}>). mount does not re-run on patches, handle_params does.
def handle_params(%{"filter" => filter}, _uri, socket) do
posts = Blog.list_posts(socket.assigns.current_scope, filter)
{:noreply, assign(socket, posts: posts, filter: filter)}
end
When the initial double-load actually matters, the real tools are:
connected?(socket) to gate work to the connected render (loses SEO / no-JS rendering)assign_async/3 to load after mount returns, in a separate processassign_new/3 to reuse values already set on conn.assigns by upstream Plugs (e.g. :current_user), or shared from a parent LiveView. It does not dedupe arbitrary work across the dead/connected boundary: the function still runs on connected mount.def mount(_params, _session, socket) do
posts = if connected?(socket), do: Blog.list_posts(socket.assigns.current_scope), else: []
{:ok, assign(socket, posts: posts)}
end
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 Gotchas section.
development
This skill should be used when the user works on any .ex or .exs file, mentions Elixir/Phoenix/Ecto/OTP, the project has a mix.exs, or asks "which skill should I use", "new to Elixir", "help with Elixir". Routes to the correct thinking skill BEFORE exploring code. Triggers on "implement", "add", "fix", "refactor" in Elixir projects.
data-ai
This skill should be used when the user asks to "add background processing", "cache this data", "run this async", "handle concurrent requests", "manage state across requests", "process jobs from a queue", "this GenServer is slow", or mentions GenServer, Supervisor, Agent, Task, Registry, DynamicSupervisor, handle_call, handle_cast, supervision trees, fault tolerance, "let it crash", or choosing between Broadway and Oban.
data-ai
This skill should be used when the user asks to "add a background job", "process async", "schedule a task", "retry failed jobs", "add email sending", "run this later", "add a cron job", "unique jobs", "batch process", or mentions Oban, Oban Pro, workflows, job queues, cascades, grafting, recorded values, job args, or troubleshooting job failures.
development
This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours, pattern matching, with statements, comprehensions, structs, or coming from an OOP background. Contains paradigm-shifting insights.