code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when building WebSocket features with Phoenix Channels — socket auth and tokens, join authorization, handle_in/push/broadcast, intercept/handle_out, Presence. Not for LiveView updates (use phoenix-pubsub-patterns).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill phoenix-channels-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/phoenix-channels-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when building WebSocket features with Phoenix Channels — socket auth and tokens, join authorization, handle_in/push/broadcast, intercept/handle_out, Presence. Not for LiveView updates (use phoenix-pubsub-patterns).
name: phoenix-channels-essentials description: "Use when building WebSocket features with Phoenix Channels — socket auth and tokens, join authorization, handle_in/push/broadcast, intercept/handle_out, Presence. Not for LiveView updates (use phoenix-pubsub-patterns)."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2 — October 2026
For non-LiveView real-time features: mobile clients, SPAs, external APIs, inter-service communication.
1. **Authenticate in `connect/3`; return `:error` otherwise.** Same-origin browser clients can reuse the phx.gen.auth session through `connect_info: [session: @session_options]`; other clients use a `Phoenix.Token` verified with an explicit `max_age` (keep it short, e.g. `86_400`; it can't be revoked). 2. **Make sockets disconnectable on logout.** `id/1` returns a topic you can broadcast `"disconnect"` to. With session auth, return the session's `live_socket_id` and the generated `log_out_user/1` already does it. 3. **Authorize in `join/3` through the scope** — load the topic's resource with `socket.assigns.current_scope` and return `{:error, %{reason: "unauthorized"}}` if that fails. The topic string is client input. 4. **Keep `check_origin` on in production.** Turning it off for a cookie-authenticated socket leaves the CSRF check as the only guard against cross-site WebSocket hijacking; Phoenix raises if both `check_origin` and `check_csrf` are `false`. 5. **Keep channel modules thin** — delegate to context functions that take the scope. 6. **Reply when the client needs the outcome** (`{:reply, {:ok, payload} | {:error, payload}, socket}`). `{:noreply, socket}` is fine for fire-and-forget events (typing indicators, the generator's `"shout"`). 7. **Use `intercept`/`handle_out` only to filter or customize a broadcast per subscriber.** An intercepted event is encoded once per subscriber instead of once per topic. 8. **Use `Phoenix.Presence` for who's online**, in channels and LiveViews. In channel tests, `assert_push "presence_state", _` before you read `Presence.list/1`.
---
# lib/my_app_web/endpoint.ex socket "/socket", MyAppWeb.UserSocket, websocket: [connect_info: [session: @session_options]], longpoll: false
`@session_options` is the attribute the generated endpoint already defines for `/live`. `check_origin` defaults to `true` (the request's `origin` must match the endpoint's `url: [host: ...]`); generated `dev.exs` turns it off. If you serve several hosts, list them in prod config:
# config/runtime.exs config :my_app, MyAppWeb.Endpoint, check_origin: ["https://example.com", "https://www.example.com"]
Native mobile clients don't send `origin`, so the check doesn't apply to them; they must use token auth.
---
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
alias MyApp.Accounts
alias MyApp.Accounts.Scope
channel "room:*", MyAppWeb.RoomChannel
@impl true
def connect(_params, socket, %{session: %{"user_token" => token} = session}) do
case Accounts.get_user_by_session_token(token) do
{user, _token_inserted_at} ->
{:ok,
socket
|> assign(:current_scope, Scope.for_user(user))
|> assign(:live_socket_id, session["live_socket_id"])}
nil ->
:error
end
end
# No session cookie, failed CSRF check (session is nil), or a guest.
def connect(_params, _socket, _connect_info), do: :error
# Same id as the LiveView socket: UserAuth.log_out_user/1 broadcasts
# "disconnect" to it, and the deleted session token blocks the reconnect.
@impl true
def id(socket), do: socket.assigns.live_socket_id
end// assets/js/user_socket.js: the session is only read if _csrf_token is sent
import {Socket} from "phoenix"
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const socket = new Socket("/socket", {params: {_csrf_token: csrfToken}})
socket.connect()
export default socketOnly sign a token for a logged-in user. A plug in the `:browser` pipeline (after `:fetch_current_scope_for_user`) keeps guests working:
# lib/my_app_web/user_auth.ex
def put_socket_token(conn, _opts) do
case conn.assigns[:current_scope] do
%{user: %{id: user_id}} ->
assign(conn, :socket_token, Phoenix.Token.sign(conn, "user socket", user_id))
_guest ->
assign(conn, :socket_token, nil)
end
end<%!-- root.html.heex, in <head> --%>
<meta :if={@socket_token} name="socket-token" content={@socket_token} />const token = document.querySelector("meta[name='socket-token']")?.content
if (token) {
const socket = new Socket("/socket", {authToken: token})
socket.connect()
}# endpoint.ex: auth_token: true sends the token in a header, not the URL
socket "/socket", MyAppWeb.UserSocket, websocket: true, longpoll: false, auth_token: true
# user_socket.ex
@impl true
def connect(_params, socket, %{auth_token: token}) when is_binary(token) do
# Always pass max_age (1 day here). Don't extend it: the token can't be revoked.
with {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 86_400),
%User{} = user <- Accounts.get_user(user_id) do
{:ok, assign(socket, :current_scope, Scope.for_user(user))}
else
_ -> :error
end
end
def connect(_params, _socket, _connect_info), do: :error
@impl true
def id(socket), do: "user_socket:#{socket.assigns.current_scope.user.id}"`Accounts.get_user/1` is `Repo.get(User, id)`, which you add; the generator only has `get_user!/1`. A mobile client sends the token from your API login response instead of a meta tag.
On logout, close that user's open sockets. Add this to the generated `log_out_user/1` before the session is renewed:
· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…