home / skills / bobmatnyc / claude-mpm-skills / phoenix-api-channels

This skill helps you design and implement Phoenix APIs, Channels, and Presence with clean boundaries and real-time capabilities.

npx playbooks add skill bobmatnyc/claude-mpm-skills --skill phoenix-api-channels

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
7.6 KB
---
name: phoenix-api-channels
description: "Phoenix controllers, JSON APIs, Channels, and Presence on the BEAM"
version: 1.0.0
category: toolchain
author: Claude MPM Team
license: MIT
progressive_disclosure:
  entry_point:
    summary: "Phoenix REST/JSON + Channels/Presence on the BEAM with contexts, plugs, auth, and PubSub."
    when_to_use:
      - "Building JSON APIs with Phoenix controllers and versioned routes"
      - "Adding WebSocket/Channels for chat, notifications, or collaborative features"
      - "Needing Presence tracking, fan-out broadcasts, and PubSub-backed updates"
      - "Integrating Ecto contexts for persistence and domain boundaries"
    quick_start:
      - "mix phx.new my_api --no-html --no-live && cd my_api"
      - "mix deps.get && mix ecto.create"
      - "Define contexts + schemas; add routes/controllers; wire `socket` + Channel modules"
      - "Start server: mix phx.server (REST + WebSocket on http://localhost:4000)"
  token_estimate:
    entry: 170
    full: 5200
---

# Phoenix APIs, Channels, and Presence (Elixir/BEAM)

Phoenix excels at REST/JSON APIs and WebSocket Channels with minimal boilerplate, leveraging the BEAM for fault tolerance, lightweight processes, and supervised PubSub/Presence.

**Core pillars**
- Controllers for JSON APIs with plugs, pipelines, and versioning.
- Contexts own data (Ecto schemas + queries) and expose a narrow API to controllers/channels.
- Channels + PubSub for fan-out real-time updates; Presence for tracking users/devices.
- Auth via plugs (session/cookie for browser, token/Bearer for APIs), with signed params.

---

## Project Setup

```bash
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.server
```

Key files:
- `lib/my_api_web/endpoint.ex` — plugs, sockets, instrumentation
- `lib/my_api_web/router.ex` — pipelines, scopes, versioning, sockets
- `lib/my_api_web/controllers/*` — REST/JSON controllers
- `lib/my_api/*` — contexts + Ecto schemas (ownership of data logic)
- `lib/my_api_web/channels/*` — Channel modules

---

## Routing and Pipelines

Separate browser vs API pipelines; version APIs with scopes.
```elixir
defmodule MyApiWeb.Router do
  use MyApiWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
    plug :protect_from_forgery
    plug MyApiWeb.Plugs.RequireAuth
  end

  scope "/api", MyApiWeb do
    pipe_through :api

    scope "/v1", V1, as: :v1 do
      resources "/users", UserController, except: [:new, :edit]
      post "/sessions", SessionController, :create
    end
  end

  socket "/socket", MyApiWeb.UserSocket,
    websocket: [connect_info: [:peer_data, :x_headers]],
    longpoll: false
end
```

**Tips**
- Keep pipelines short; push auth/guards into plugs.
- Expose `socket "/socket"` for Channels; restrict transports as needed.

---

## Controllers and Plugs

Controllers stay thin; contexts own the logic.
```elixir
defmodule MyApiWeb.V1.UserController do
  use MyApiWeb, :controller
  alias MyApi.Accounts

  action_fallback MyApiWeb.FallbackController

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, :index, users: users)
  end

  def create(conn, params) do
    with {:ok, user} <- Accounts.register_user(params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
      |> render(:show, user: user)
    end
  end
end
```

**FallbackController** centralizes error translation (`{:error, :not_found}` → 404 JSON).

**Plugs**
- `RequireAuth` verifies bearer/session tokens, sets `current_user`.
- Use `plug :scrub_params`-style transforms in pipelines, not controllers.
- Avoid heavy work in plugs; they run per-request.

---

## Contexts and Data (Ecto)

Contexts expose only what controllers/channels need.
```elixir
defmodule MyApi.Accounts do
  import Ecto.Query, warn: false
  alias MyApi.{Repo, Accounts.User}

  def list_users, do: Repo.all(User)
  def get_user!(id), do: Repo.get!(User, id)

  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end
end
```

**Guidelines**
- Keep schema modules free of controller knowledge.
- Validate at the changeset; use `Ecto.Multi` for multi-step operations.
- Prefer pagination helpers (`Scrivener`, `Flop`) for large lists.

---

## Channels, PubSub, and Presence

Channel module example:
```elixir
defmodule MyApiWeb.RoomChannel do
  use Phoenix.Channel
  alias Phoenix.Presence

  def join("room:" <> room_id, _payload, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_info(:after_join, socket) do
    Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
    push(socket, "presence_state", Presence.list(socket))
    {:noreply, socket}
  end

  def handle_in("message:new", %{"body" => body}, socket) do
    broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
    {:noreply, socket}
  end
end
```

**PubSub from contexts**
```elixir
def create_order(attrs) do
  with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
    Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
    {:ok, order}
  end
end
```

**Best practices**
- Authorize in `UserSocket.connect/3` before joining topics.
- Limit payload sizes; validate incoming events.
- Use topic partitioning for tenancy (`"tenant:" <> tenant_id <> ":room:" <> room_id`).

---

## Authentication Patterns

- **API tokens**: Accept `authorization: Bearer <token>`; verify in plug, assign `current_user`.
- **Signed params**: `Phoenix.Token.sign/verify` for short-lived join params.
- **Rate limiting**: Use plugs + ETS/Cachex or reverse proxy (NGINX/Cloudflare).
- **CORS**: Configure in `Endpoint` with `cors_plug`.

---

## Testing

Use generated helpers:
```elixir
defmodule MyApiWeb.UserControllerTest do
  use MyApiWeb.ConnCase, async: true

  test "lists users", %{conn: conn} do
    conn = get(conn, ~p\"/api/v1/users\")
    assert json_response(conn, 200)["data"] == []
  end
end
```

Channel tests:
```elixir
defmodule MyApiWeb.RoomChannelTest do
  use MyApiWeb.ChannelCase, async: true

  test "broadcasts messages" do
    {:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
    {:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
    ref = push(socket, "message:new", %{"body" => "hi"})
    assert_reply ref, :ok
    assert_broadcast "message:new", %{body: "hi"}
  end
end
```

**DataCase**: isolates DB per test; use fixtures/factories for setup.

---

## Telemetry, Observability, and Ops

- `:telemetry` events from endpoint, controller, channel, and Ecto queries; export via `OpentelemetryPhoenix` and `OpentelemetryEcto`.
- Use `Plug.Telemetry` for request metrics; add logging metadata (request_id, user_id).
- Releases: `MIX_ENV=prod mix release`; configure runtime in `config/runtime.exs`.
- Clustering: `libcluster` + distributed PubSub for multi-node Presence.
- Assetless APIs: disable unused watchers (esbuild/tailwind) for API-only apps.

---

## Common Pitfalls

- Controllers doing queries directly instead of delegating to contexts.
- Not authorizing in `UserSocket.connect/3`, leading to topic exposure.
- Missing `action_fallback` → inconsistent error shapes.
- Forgetting to limit event payloads; large messages can overwhelm channels.
- Leaving longpoll enabled when unused; disable to reduce surface area.

Phoenix API + Channels shine when contexts own data, controllers stay thin, and Channels use PubSub/Presence with strict authorization and telemetry. The BEAM handles concurrency and fault tolerance; focus on clear boundaries and real-time experiences.

Overview

This skill documents patterns for building Phoenix JSON APIs, WebSocket Channels, and Presence on the BEAM. It focuses on thin controllers, context-driven data access, Channel/PubSub flows, and practical auth and testing patterns. Use it to design fault-tolerant, real-time backends with clear boundaries and observability.

How this skill works

The skill inspects and explains core Phoenix components: router pipelines, controller/plug responsibilities, Ecto-backed contexts, Channel modules, PubSub broadcasts, and Presence tracking. It shows how to structure pipelines, authorize socket connections, broadcast from contexts, and test controllers and channels. It also covers telemetry, releases, clustering, and common operational concerns.

When to use it

  • Building JSON REST APIs with versioned endpoints
  • Adding real-time features via Channels and Presence
  • Implementing token/session authentication for browser and API clients
  • Designing context-driven domain logic with Ecto and transactions
  • Testing controllers and channels in isolation with ConnCase/ChannelCase
  • Instrumenting telemetry and preparing production releases

Best practices

  • Keep controllers thin; move DB logic into contexts and use Ecto.Multi for multi-step changes
  • Use short, focused plugs for auth and request transforms; avoid heavy work in plugs
  • Authorize in UserSocket.connect/3 and validate signed join params with Phoenix.Token
  • Broadcast domain events from contexts using Phoenix.PubSub, not directly from controllers
  • Limit and validate incoming channel payloads; partition topics for tenancy to reduce blast radius
  • Add Plug.Telemetry and Ecto telemetry exporters for metrics, and include request_id/user_id in logs

Example use cases

  • Expose versioned API routes under /api/v1 with an :api pipeline that accepts JSON and requires auth
  • Implement a chat RoomChannel that tracks presence, broadcasts messages, and pushes presence_state after join
  • Publish order_created events from an Orders context so workers and channels react to new orders
  • Use Phoenix.Token for short-lived socket joins and verify bearer tokens in an API RequireAuth plug
  • Write ChannelCase tests that connect a socket, subscribe_and_join a topic, push an event, and assert replies and broadcasts

FAQ

How should I authenticate socket connections?

Verify tokens or session data in UserSocket.connect/3, assign current_user on the socket, and reject unauthorized connects before allowing topic joins.

Where should I broadcast real-time events from?

Broadcast from contexts after successful DB changes so domain logic owns side effects; contexts can call Phoenix.PubSub.broadcast/3 with well-defined topics.