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


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
Subscribe to my newsletter
Read articles from BetaMize directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
