How to Use Phoenix Channels: Real-Time Chat with Elixir

BetaMizeBetaMize
4 min read

Phoenix Channels enable developers to build scalable, real-time applications in Elixir. Backed by the concurrency power of the BEAM, Channels allow you to create chat apps, live dashboards, games, and more with low latency and high throughput.

In this article, weโ€™ll explore:

  • What are Phoenix Channels?

  • When to use them

  • Step-by-step implementation

  • Key functions with explanations

  • Real-time communication in action

  • Best practices

  • Final thoughts and CTA


๐Ÿ” What Are Phoenix Channels?

Phoenix Channels provide a simple yet powerful abstraction over WebSockets. They are designed for bi-directional communication between clients (like web browsers) and the server, using a topic-based pattern.

  • Channels are mounted on a Socket

  • Each Channel handles a topic (e.g., "room:lobby", "user:42")

  • One socket connection can handle multiple topics


๐Ÿ’ก When to Use Phoenix Channels

Use Channels when your app needs:

  • Real-time updates (chat, notifications, dashboards)

  • Multiplayer collaboration

  • Interactive interfaces (live auctions, stock updates)

  • Background broadcasting of messages to clients


โš™๏ธ Step-by-Step: Create a Real-Time Chat with Phoenix Channels

Letโ€™s implement a real-time chat room.

1. Create a Phoenix Project

mix phx.new chat_app --no-ecto
cd chat_app
mix deps.get

๐Ÿ”Œ Server-Side: Understanding Channel Functions

File: lib/chat_app_web/channels/room_channel.ex

defmodule ChatAppWeb.RoomChannel do
  use ChatAppWeb, :channel

  def join("room:lobby", _payload, socket) do
    {:ok, socket}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast(socket, "new_msg", %{body: body})
    {:noreply, socket}
  end
end

๐Ÿ” Explanation:

๐Ÿ”น join(topic, payload, socket)

  • What it does: Called when a client tries to join a topic like "room:lobby".

  • Purpose: Authenticate, authorize, and allow/deny access to a topic.

  • Returns:

    • {:ok, socket} if the client can join

    • {:error, reason} if access should be denied

def join("room:lobby", _payload, socket) do
  if authorized?(socket) do
    {:ok, socket}
  else
    {:error, %{reason: "unauthorized"}}
  end
end

๐Ÿ”น handle_in(event, params, socket)

  • What it does: Handles an incoming message (pushed by the client).

  • Example: When a client pushes "new_msg" with body "Hi", this function is triggered.

def handle_in("new_msg", %{"body" => body}, socket) do
  broadcast(socket, "new_msg", %{body: body})
  {:noreply, socket}
end

๐Ÿ”น broadcast(socket, event, payload)

  • What it does: Sends the payload to all other clients connected to the same topic.

  • Used to notify all participants of a change or message.

broadcast(socket, "new_msg", %{body: "Hello, all!"})

Connect the Channel

In lib/chat_app_web/channels/user_socket.ex:

channel "room:*", ChatAppWeb.RoomChannel

This tells Phoenix to route any topic starting with "room:" to the RoomChannel.

๐Ÿ” Explanation of user_socket.ex:

This file acts as the entry point for all WebSocket connections. It defines:


๐Ÿ”ธ connect(params, socket, connect_info)

def connect(_params, socket, _connect_info) do
  {:ok, socket}
end
  • Authenticates users when they first open the WebSocket.

  • You can use tokens or session info to verify users.


๐Ÿ”ธ id(socket)

def id(_socket), do: nil
  • Purpose: Assigns a unique identifier for each socket.

  • Useful when you want to push a message to a specific user.

Example with ID:

def id(socket), do: "user_socket:#{socket.assigns.user_id}"

Then you can broadcast to a specific user like:

ChatAppWeb.Endpoint.broadcast("user_socket:123", "new_notification", %{msg: "Hello!"})

๐ŸŒ Client-Side: JavaScript Integration

1. Import and Connect

import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {userToken: "123"}})
socket.connect()

2. Join a Channel

let channel = socket.channel("room:lobby", {})
channel.join()
  .receive("ok", resp => console.log("Joined successfully", resp))
  .receive("error", resp => console.log("Unable to join", resp))

3. Push Data to Server

channel.push("new_msg", {body: "Hello from client!"})

This triggers the handle_in("new_msg", ...) on the server.


4. Receive Broadcasts

channel.on("new_msg", payload => {
  const message = document.createElement("p")
  message.innerText = `[New] ${payload.body}`
  document.querySelector("#messages").appendChild(message)
})

๐Ÿ—ฃ Broadcasting from Server

Broadcast from anywhere in the app using:

ChatAppWeb.Endpoint.broadcast("room:lobby", "new_msg", %{body: "Hello from server!"})

This doesn't need a client push; it works independently โ€” perfect for admin messages or system events.


๐Ÿ” Authorization Example

defp authorized?(socket) do
  # Validate token or user role
  true
end

Use this pattern inside join/3 to limit access to sensitive channels.


โœ… Best Practices

  • Validate join requests to prevent unauthorized access.

  • Use topic conventions ("user:123", "room:lobby") for scalability.

  • Avoid large payloads to keep WebSocket communication fast.

  • Log channel events in production for debugging.

  • Use Presence module if you need to track online users.


๐Ÿš€ What Can You Build?

  • Live chat apps

  • Multiplayer games

  • Real-time dashboards

  • Notification systems

  • Collaborative editors (Google Docs style)


๐Ÿ”š Conclusion

Phoenix Channels make it easy to build real-time, scalable applications with a clean and efficient API. By leveraging Elixirโ€™s concurrency model, you can build fast, reliable systems that handle thousands of connections with minimal resource usage.


๐Ÿค Work with the Experts

Looking to build a real-time app with Phoenix Channels?

ElixirMasters (powered by BetaMize) can help you:

โœ… Build scalable real-time web apps
โœ… Develop reliable backends using Phoenix
โœ… Maintain and optimize Elixir applications

๐Ÿ‘‰ Let's Talk โ€“ elixirmasters.com

0
Subscribe to my newsletter

Read articles from BetaMize directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

BetaMize
BetaMize