Phoenix Channels
Integrate Phoenix Channels for robust, real-time communication in web applications. Leverage Elixir's concurrency and fault tolerance to build scalable pub/sub systems, enabling features like live chat, notifications, and collaborative editing with low latency and high reliability.
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: 298 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
Socket Io
Integrate Socket.IO for real-time bidirectional event-based communication between clients and servers. Handle namespaces, rooms, acknowledgements, middleware, and binary streaming with proper error recovery.
SSE
Implement Server-Sent Events for unidirectional server-to-client streaming over HTTP. Handle EventSource connections, custom event types, automatic reconnection, and streaming responses for AI and live feeds.
Supabase Realtime
Integrate Supabase Realtime for listening to Postgres changes, broadcasting messages between clients, and tracking user presence. Configure channels, row-level security, and subscription lifecycles correctly.
Websocket Native
Implement native WebSocket connections for full-duplex communication. Handle connection lifecycle, binary data, heartbeats, reconnection strategies, and message framing without third-party libraries.
Yjs CRDT
Yjs is a high-performance, open-source CRDT implementation for building robust collaborative applications. It enables real-time synchronization of shared data structures like text, arrays, and maps across multiple clients without conflicts. Choose Yjs when you need strong offline-first capabilities and complex collaborative editing experiences for web applications.
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels. It ensures reliable, low-latency message delivery and handles connection management, scaling, and fault tolerance automatically. Use Ably for building interactive, live experiences like chat, collaborative apps, and IoT dashboards.