Phoenix Channels
Integrate Phoenix Channels for robust, real-time communication in web applications. Leverage Elixir's
You are an expert in building highly scalable and fault-tolerant real-time features using Phoenix Channels. You understand how to harness Elixir's BEAM VM for concurrent connections, manage channel lifecycles, and implement secure, topic-based publish/subscribe patterns. You design client-side integrations that are resilient to network issues, handle authentication seamlessly, and provide immediate user feedback for dynamic web experiences. ## Quick Example ```bash npm install phoenix # or yarn add phoenix ```
skilldb get realtime-services-skills/Phoenix ChannelsFull skill: 299 linesYou are an expert in building highly scalable and fault-tolerant real-time features using Phoenix Channels. You understand how to harness Elixir's BEAM VM for concurrent connections, manage channel lifecycles, and implement secure, topic-based publish/subscribe patterns. You design client-side integrations that are resilient to network issues, handle authentication seamlessly, and provide immediate user feedback for dynamic web experiences.
Core Philosophy
Phoenix Channels are built upon Elixir's strengths: concurrency, fault tolerance, and a robust process model. Unlike traditional HTTP request/response, Channels provide full-duplex, persistent connections via WebSockets, allowing both the server and client to push messages at any time. This architecture makes them ideal for applications requiring instant updates, such as chat applications, live dashboards, and collaborative tools. You choose Phoenix Channels when you need a real-time backend that is inherently scalable and resilient without relying on sticky sessions or complex external message brokers for basic pub/sub.
The core abstraction is the "channel," which represents a topic of communication. Clients "join" a channel, specifying a topic string (e.g., room:lobby, user:123:notifications), and can then send and receive events on that topic. The server-side channel process manages subscriptions, authorizes joins, and broadcasts messages to all connected clients on that topic. This topic-based approach simplifies message routing and allows you to structure your real-time data flow logically, ensuring that clients only receive events relevant to their current context or permissions.
Phoenix Channels also provide built-in mechanisms for handling connection state, including automatic client-side reconnection with optional backoff, and server-side presence tracking. This reduces the boilerplate often associated with real-time systems, allowing you to focus on application logic. The client-side JavaScript SDK (phoenix.js) abstracts away much of the WebSocket management, offering a convenient API for joining channels, pushing events, and listening for incoming messages, while gracefully handling network interruptions.
Setup
To get started with Phoenix Channels, you need both a server-side Phoenix application and a client-side JavaScript application.
1. Server-side (Elixir Phoenix): First, generate a channel and configure your socket in a Phoenix project.
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
# Authenticate and authorize the join
def join("room:" <> _room_id, _params, socket) do
# You might check user authentication here using `socket.assigns.user_id`
# or by inspecting `params` for a token.
# If unauthorized, return {:error, %{reason: "unauthorized"}}
{:ok, socket}
end
# Handle incoming messages from the client
def handle_in("new_msg", %{"body" => body}, socket) do
# Broadcast the message to all clients in this channel topic
broadcast!(socket, "new_msg", %{body: body, sender: socket.assigns.user_id})
{:noreply, socket}
end
# Handle other events or private messages
# ...
end
Then, configure your UserSocket and Endpoint to route WebSocket connections.
# lib/my_app_web/channels/user_socket.ex
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", MyAppWeb.RoomChannel
channel "user:*", MyAppWeb.UserChannel # Example for user-specific channels
# Socket authentication:
# This function is called when a client attempts to connect to the socket.
# You can use a JWT token passed in the connection params or session.
def connect(%{"token" => token}, socket, _connect_info) do
case MyAppWeb.Auth.validate_token(token) do
{:ok, user_id} ->
{:ok, assign(socket, :user_id, user_id)}
_ ->
:error
end
end
def connect(_params, _socket, _connect_info) do
:error
end
# ... (Presence and other callbacks)
end
# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
socket "/socket", MyAppWeb.UserSocket,
websocket: true,
longpoll: false # Set to true for environments without WebSocket support
# ... other endpoint configurations
end
2. Client-side (JavaScript):
Install the phoenix package and initialize your socket.
npm install phoenix
# or
yarn add phoenix
// src/socket.js (or wherever your client-side JS lives)
import { Socket } from "phoenix";
// Initialize the socket connection
// Provide a JWT token for authentication in the params
const socket = new Socket("/socket", {
params: { token: localStorage.getItem("user_jwt") },
});
// Connect the socket
socket.connect();
// Optional: monitor socket connection status
socket.onOpen(() => console.log("Socket connected successfully!"));
socket.onClose(() => console.log("Socket disconnected."));
socket.onError((error) => console.error("Socket error:", error));
socket.onMessage((msg) => console.log("Socket received message:", msg));
export default socket;
Key Techniques
1. Joining and Interacting with a Channel
Once your socket is connected, you can join specific channels, send events, and listen for incoming messages.
// src/chat.js (example component for a chat room)
import socket from "./socket";
// Define the channel topic
const roomTopic = "room:lobby";
const channel = socket.channel(roomTopic, {}); // No extra join params for this example
// Join the channel
channel
.join()
.receive("ok", (resp) => {
console.log(`Joined channel ${roomTopic} successfully`, resp);
// You can now send messages
channel.push("new_msg", { body: "Hello Phoenix!" });
})
.receive("error", (resp) => {
console.error(`Unable to join channel ${roomTopic}`, resp);
})
.receive("timeout", () => console.warn("Channel join timed out"));
// Listen for incoming messages from the server on this channel
channel.on("new_msg", (payload) => {
console.log("New message received:", payload.body, "from", payload.sender);
// Update your UI with the new message
});
// Listen for the channel closing unexpectedly
channel.onClose(() => {
console.log(`Channel ${roomTopic} closed unexpectedly.`);
// Handle UI updates or attempt re-join if desired (Phoenix.js handles socket reconnects automatically)
});
// Function to send a message
export function sendMessage(messageBody) {
channel
.push("new_msg", { body: messageBody })
.receive("ok", () => console.log("Message sent successfully!"))
.receive("error", (reasons) => console.error("Failed to send message", reasons))
.receive("timeout", () => console.warn("Sending message timed out"));
}
2. Handling User-Specific Notifications
For private, user-specific notifications, you typically create a channel topic that includes the user's ID.
// src/notifications.js
import socket from "./socket";
function setupUserNotifications(userId) {
const userTopic = `user:${userId}:notifications`;
const notificationChannel = socket.channel(userTopic, {});
notificationChannel
.join()
.receive("ok", () => console.log(`Joined user notification channel ${userTopic}`))
.receive("error", (resp) => console.error(`Failed to join user channel ${userTopic}`, resp));
notificationChannel.on("new_notification", (payload) => {
console.log("Received new notification:", payload);
// Display the notification in your UI, e.g., a toast or badge update
alert(`Notification: ${payload.message}`);
});
notificationChannel.on("unread_count_update", (payload) => {
console.log("Unread count updated:", payload.count);
// Update unread notification badge
});
return notificationChannel;
}
// Example usage
// Assuming 'currentUserId' is retrieved from your authenticated session
const currentUserId = "123"; // Replace with actual user ID
const userNotificationChannel = setupUserNotifications(currentUserId);
# lib/my_app_web/channels/user_channel.ex
defmodule MyAppWeb.UserChannel do
use MyAppWeb, :channel
# Ensure only the authenticated user can join their own channel
def join("user:" <> user_id_str, _params, socket) do
user_id = String.to_integer(user_id_str)
if socket.assigns.user_id == user_id do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
# Example function to push a notification to a specific user
def notify_user(user_id, message) do
topic = "user:#{user_id}:notifications"
# Push directly to the topic. broadcast! ensures delivery to all connected clients on that topic.
MyAppWeb.Endpoint.broadcast!(topic, "new_notification", %{message: message, timestamp: NaiveDateTime.utc_now()})
end
def update_unread_count(user_id, count) do
topic = "user:#{user_id}:notifications"
MyAppWeb.Endpoint.broadcast!(topic, "unread_count_update", %{count: count})
end
end
3. Reconnection Strategies and State Management
phoenix.js provides built-in reconnection logic. You can configure its behavior and react to connection state changes.
// src/socket.js (enhanced with reconnection config)
import { Socket } from "phoenix";
const socket = new Socket("/socket", {
params: { token: localStorage.getItem("user_jwt") },
// Optional: Configure reconnection strategy
reconnectAfterMs: (tries) => {
// Exponential backoff with jitter
const minMs = 100;
const maxMs = 5000;
const jitter = Math.random() * minMs;
return Math.min(maxMs, minMs * Math.pow(2, tries) + jitter);
},
});
socket.connect();
let isConnected = false;
socket.onOpen(() => {
console.log("Socket connected.");
isConnected = true;
// If you had buffered messages, send them now
// For channels, they will auto-rejoin if the socket reconnects
});
socket.onClose(() => {
console.log("Socket disconnected.");
isConnected = false;
// Inform user that connection is lost, UI might show a "reconnecting..." status
});
socket.onError((error) => {
console.error("Socket error:", error);
// Log critical errors. phoenix.js attempts reconnects automatically.
});
// Example of how you might manage state based on connection
export function getSocketStatus() {
return isConnected ? "connected" : "disconnected";
}
When a channel's underlying socket reconnects, phoenix.js automatically attempts to rejoin all previously joined channels. This means your client-side code generally doesn't need to manually re-call channel.join() after a temporary network outage. However, you should still handle receive("error") and receive("timeout") on the initial join() and subsequent push() calls for user feedback
Anti-Patterns
Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.
Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.
Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.
Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.
Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.
Install this skill directly: skilldb add realtime-services-skills
Related Skills
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels.
Centrifugo
Centrifugo is a high-performance, real-time messaging server that handles WebSocket,
Convex Realtime
Integrate Convex for a real-time backend with reactive queries, transactional mutations, and automatic
Electric SQL
Integrate ElectricSQL to build local-first, real-time applications with a PostgreSQL backend.
Firebase Realtime Db
Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,
Liveblocks
Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage,