---
name: phoenix-channels-essentials
description: Use when building WebSocket features with Phoenix Channels — socket auth, join authorization, handle_in/push/broadcast, Presence.
file_patterns:
  - "**/*_socket.ex"
  - "**/*_channel.ex"
  - "**/channels/**/*.ex"
auto_suggest: true
---

# Phoenix Channels Essentials

For non-LiveView real-time features: mobile clients, SPAs, external APIs, inter-service communication.

## RULES — Follow these with no exceptions

1. **Always authenticate in `connect/3`** — channels bypass the Plug pipeline; tokens must be verified in the socket
2. **Authorize in `join/3`** — verify the user can access the requested topic before allowing the connection
3. **Use `handle_in` for client-to-server, `push` for server-to-client, `broadcast` for server-to-all** — never confuse the direction
4. **Keep channel modules thin** — delegate business logic to context modules; channels are the transport layer
5. **Use Presence for tracking connected users** — don't roll your own presence tracking; Phoenix.Presence handles node distribution
6. **Return `{:reply, :ok, socket}` or `{:reply, {:error, reason}, socket}` from `handle_in`** — don't silently drop messages

---

## Socket Authentication

Channels bypass the Plug pipeline, so session-based auth doesn't work. Use token-based authentication.

### Generating Tokens (Server Side)

```elixir
# In a controller or LiveView — generate a token for the current user
defmodule MyAppWeb.UserAuth do
  def generate_socket_token(conn) do
    Phoenix.Token.sign(conn, "user socket", conn.assigns.current_scope.user.id)
  end
end

# In your layout or root template
<script>
  window.userToken = "<%= Phoenix.Token.sign(@conn, "user socket", @current_scope.user.id) %>"
</script>
```

### Verifying Tokens (Socket)

```elixir
defmodule MyAppWeb.UserSocket do
  use Phoenix.Socket

  channel "room:*", MyAppWeb.RoomChannel
  channel "notifications:*", MyAppWeb.NotificationChannel

  @impl true
  def connect(%{"token" => token}, socket, _connect_info) do
    # Phoenix.Token.verify/4 defaults max_age to 86_400 (1 day); this example
    # passes max_age: 1_209_600 to extend it to 2 weeks.
    case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do
      {:ok, user_id} ->
        {:ok, assign(socket, :user_id, user_id)}

      {:error, _reason} ->
        :error
    end
  end

  def connect(_params, _socket, _connect_info), do: :error

  @impl true
  def id(socket), do: "users_socket:#{socket.assigns.user_id}"
end
```

**Bad:**
```elixir
# No authentication — anyone can connect
def connect(_params, socket, _connect_info) do
  {:ok, socket}
end
```

---

## Topic Authorization

Verify in `join/3` that the user is allowed to access the topic.

```elixir
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  @impl true
  def join("room:" <> room_id, _payload, socket) do
    user_id = socket.assigns.user_id

    if Rooms.member?(room_id, user_id) do
      {:ok, assign(socket, :room_id, room_id)}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end
end
```

**Bad:**
```elixir
# No authorization — any authenticated user can join any room
def join("room:" <> room_id, _payload, socket) do
  {:ok, assign(socket, :room_id, room_id)}
end
```

---

## Channel Message Patterns

### Client-to-Server (handle_in)

Always reply so the client knows the result.

```elixir
@impl true
def handle_in("new_msg", %{"body" => body}, socket) do
  user_id = socket.assigns.user_id
  room_id = socket.assigns.room_id

  case Chat.create_message(room_id, user_id, body) do
    {:ok, message} ->
      broadcast!(socket, "new_msg", %{
        id: message.id,
        body: message.body,
        user_id: message.user_id,
        inserted_at: message.inserted_at
      })
      {:reply, :ok, socket}

    {:error, changeset} ->
      {:reply, {:error, %{errors: format_errors(changeset)}}, socket}
  end
end
```

**Bad:**
```elixir
# No reply — client doesn't know if message was received
def handle_in("new_msg", %{"body" => body}, socket) do
  broadcast!(socket, "new_msg", %{body: body})
  {:noreply, socket}
end
```

### Server-to-Client (push)

Send a message to a specific client, not everyone.

```elixir
# Push to this specific client only
push(socket, "typing", %{user_id: other_user_id})

# Broadcast to all clients on the topic (including sender)
broadcast!(socket, "new_msg", payload)

# Broadcast to all clients except the sender
broadcast_from!(socket, "user_joined", %{user_id: user_id})
```

### External Messages (handle_info)

For messages from PubSub, timers, or other processes.

```elixir
@impl true
def handle_info({:new_notification, notification}, socket) do
  push(socket, "notification", %{
    title: notification.title,
    body: notification.body
  })
  {:noreply, socket}
end
```

---

## Topic Naming Conventions

```elixir
# Resource-specific — one room
"room:42"

# User-scoped — all notifications for a user
"notifications:user_123"

# Collection-wide — all public updates
"updates:all"

# Subtopic — specific channel within a room
"room:42:typing"
```

**Pattern match in join to extract IDs:**
```elixir
def join("room:" <> room_id, _payload, socket) do
  # room_id is a string — parse if needed
  room_id = String.to_integer(room_id)
  # ...
end
```

---

## Presence Tracking

Use `Phoenix.Presence` for tracking who is online. It handles distributed nodes automatically.

### Setup

```elixir
# lib/my_app_web/channels/presence.ex
defmodule MyAppWeb.Presence do
  use Phoenix.Presence,
    otp_app: :my_app,
    pubsub_server: MyApp.PubSub
end
```

### Tracking in a Channel

```elixir
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel
  alias MyAppWeb.Presence

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

  @impl true
  def handle_info(:after_join, socket) do
    # Track this user's presence
    {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
      online_at: inspect(System.system_time(:second)),
      typing: false
    })

    # Send current presence state to the joining client
    push(socket, "presence_state", Presence.list(socket))

    {:noreply, socket}
  end
end
```

### Updating Presence Metadata

```elixir
@impl true
def handle_in("typing", %{"typing" => typing}, socket) do
  Presence.update(socket, socket.assigns.user_id, fn meta ->
    Map.put(meta, :typing, typing)
  end)
  {:reply, :ok, socket}
end
```

---

## When to Use Channels vs LiveView vs PubSub

| Feature | Channels | LiveView | PubSub |
|---------|----------|----------|--------|
| **Client** | Any (mobile, SPA, IoT) | Browser only | Server-side only |
| **Protocol** | WebSocket + custom | WebSocket + HTML | Erlang messages |
| **Rendering** | Client renders | Server renders | No rendering |
| **Use when** | Non-browser clients, custom protocols | Browser UI with real-time | Inter-process communication |

**Choose Channels when:**
- Mobile apps need real-time features
- SPA frontend (React, Vue) needs WebSocket communication
- External services need bidirectional communication
- You need a custom binary protocol

**Choose LiveView when:**
- Browser-based UI with real-time updates
- Server-rendered HTML is acceptable
- You want to avoid writing JavaScript

**Choose PubSub when:**
- Server-side inter-process communication only
- LiveView components need to communicate
- Background jobs need to notify the web layer

---

## Testing Channels

```elixir
defmodule MyAppWeb.RoomChannelTest do
  use MyAppWeb.ChannelCase

  setup do
    user = user_fixture()
    room = room_fixture(members: [user])
    token = Phoenix.Token.sign(MyAppWeb.Endpoint, "user socket", user.id)
    {:ok, socket} = connect(MyAppWeb.UserSocket, %{"token" => token})
    {:ok, _, socket} = subscribe_and_join(socket, "room:#{room.id}", %{})

    %{socket: socket, user: user, room: room}
  end

  test "new_msg broadcasts to room", %{socket: socket} do
    ref = push(socket, "new_msg", %{"body" => "hello"})

    assert_reply ref, :ok
    assert_broadcast "new_msg", %{body: "hello"}
  end

  test "new_msg with invalid data returns error", %{socket: socket} do
    ref = push(socket, "new_msg", %{"body" => ""})

    assert_reply ref, :error, %{errors: _}
  end

  test "unauthorized user cannot join room", %{socket: socket, room: room} do
    other_user = user_fixture()
    token = Phoenix.Token.sign(MyAppWeb.Endpoint, "user socket", other_user.id)
    {:ok, socket} = connect(MyAppWeb.UserSocket, %{"token" => token})

    assert {:error, %{reason: "unauthorized"}} =
             subscribe_and_join(socket, "room:#{room.id}", %{})
  end

  test "presence is tracked on join", %{socket: socket, user: user} do
    user_id = to_string(user.id)
    assert %{^user_id => %{metas: [%{online_at: _}]}} =
             MyAppWeb.Presence.list(socket)
  end
end
```

---

See `phoenix-pubsub-patterns` skill for server-side PubSub patterns.
See `phoenix-liveview-essentials` skill for LiveView real-time patterns.
See `testing-essentials` skill for comprehensive testing patterns.
