Quick Start
This guide walks you through building a working real-time channel from scratch: a Gleam server that handles join and message events, wired to a Phoenix JS client running in the browser.
The snippets here are abbreviated to keep things readable. For a fully runnable application with HTML, static assets, and end-to-end tests, see the examples.
Prerequisites
Section titled “Prerequisites”- Gleam project targeting Erlang (
gleam new my_app) gleam add beryladds beryl and all its dependencies
1. Define a channel
Section titled “1. Define a channel”A channel is a Gleam module that returns a Channel(assigns) value. The assigns
type holds per-socket state — anything you want to remember about this connection.
import berylimport beryl/channel.{type Channel, type HandleResult, type JoinResult}import beryl/socket.{type Socket}import gleam/dynamic/decodeimport gleam/jsonimport gleam/option.{None, Some}
/// Per-socket state for this channel.pub type RoomAssigns { RoomAssigns(username: String, channels: beryl.Channels, topic: String)}
/// Build the channel handler.pub fn new(channels: beryl.Channels) -> Channel(RoomAssigns) { channel.new(fn(topic, payload, socket) { join(channels, topic, payload, socket) }) |> channel.with_handle_in(handle_in)}
/// Called when a client sends a join request for a matching topic.fn join( channels: beryl.Channels, topic: String, payload: json.Json, socket: Socket(RoomAssigns),) -> JoinResult(RoomAssigns) { // Extract username from the join payload, default to "Anonymous". let username = case json.parse(json.to_string(payload), { use u <- decode.field("username", decode.string) decode.success(u) }) { Ok(u) -> u Error(_) -> "Anonymous" }
let assigns = RoomAssigns(username:, channels:, topic:) let socket = socket.set_assigns(socket, assigns)
// Send back a reply — delivered to the client as phx_reply on the join ref. channel.JoinOk( reply: Some(json.object([#("username", json.string(username))])), socket:, )}
/// Called for every push the client sends after joining.fn handle_in( event: String, payload: json.Json, socket: Socket(RoomAssigns),) -> HandleResult(RoomAssigns) { let assigns = socket.get_assigns(socket)
case event { "new_msg" -> { // Broadcast to every socket joined to this topic (including the sender). beryl.broadcast(assigns.channels, assigns.topic, "new_msg", payload) // Return NoReply — phx_reply is NOT sent for broadcasts. channel.NoReply(socket) } _ -> channel.NoReply(socket) }}2. Start beryl and register the channel
Section titled “2. Start beryl and register the channel”Wire everything together in your application entry point:
import berylimport beryl/transport/mist as mist_transportimport gleam/bytes_treeimport gleam/erlang/processimport gleam/http/requestimport gleam/http/responseimport mistimport my_app/room_channel
pub fn main() { // Start the beryl channel system. let assert Ok(channels) = beryl.start(beryl.default_config())
// Register the room channel for all "room:*" topics. let assert Ok(_) = beryl.register(channels, "room:*", room_channel.new(channels))
// Start the HTTP + WebSocket server. let assert Ok(_) = fn(req) { // Upgrade WebSocket requests first; fall through to HTTP for everything else. mist_transport.upgrade( req, channels.coordinator, mist_transport.default_config("/socket/websocket"), fn() { handle_http(req) }, ) } |> mist.new |> mist.port(8000) |> mist.start
process.sleep_forever()}
fn handle_http( req: request.Request(mist.Connection),) -> response.Response(mist.ResponseData) { case request.path_segments(req) { [] -> response.new(200) |> response.set_body(mist.Bytes(bytes_tree.from_string("Hello!"))) _ -> response.new(404) |> response.set_body(mist.Bytes(bytes_tree.new())) }}3. Connect from the browser
Section titled “3. Connect from the browser”beryl uses the same wire format as Phoenix channels, so you can use the official
phoenix npm package (or CDN build) in your frontend.
npm install phoenix<script src="https://cdn.jsdelivr.net/npm/phoenix/priv/static/phoenix.min.js"></script>import { Socket } from "phoenix";
// "/socket" → client appends "/websocket" → hits "/socket/websocket" on the server.const socket = new Socket("/socket");socket.connect();
// Join the "room:lobby" topic.const channel = socket.channel("room:lobby", { username: "alice" });
channel .join() .receive("ok", (resp) => { // resp is the payload from channel.JoinOk reply — { username: "alice" } console.log("Joined as", resp.username); }) .receive("error", (resp) => { console.error("Join failed", resp); });
// Listen for broadcast messages.channel.on("new_msg", (payload) => { console.log("Message:", payload);});
// Send a message.channel.push("new_msg", { text: "Hello, world!" });4. Acknowledging a specific push
Section titled “4. Acknowledging a specific push”If you want the server to confirm delivery of an individual push, return
channel.Reply from handle_in. The client receives a phx_reply event tied
to the original message ref:
// Server side — acknowledge message delivery"new_msg" -> { beryl.broadcast(assigns.channels, assigns.topic, "new_msg", payload) channel.Reply( event: "msg_ack", // event is ignored in the wire protocol; only payload matters payload: json.object([#("status", json.string("ok"))]), socket:, )}// Client side — receive the acknowledgmentchannel .push("new_msg", { text: "Hello!" }) .receive("ok", (resp) => { // resp is the payload from channel.Reply — { status: "ok" } console.log("Delivered", resp); });5. Rejecting a join
Section titled “5. Rejecting a join”Return channel.JoinError from your join callback to refuse a client:
fn join(...) -> JoinResult(RoomAssigns) { case is_room_valid(topic) { False -> channel.JoinError(reason: channel.error("Room not found")) True -> // ... proceed as normal }}The client receives an "error" reply on its .join() call.
Next steps
Section titled “Next steps”- Explore the full examples — two runnable demos with HTML frontends
- Learn about Channels in depth — topic patterns, handle_out, terminate
- Add Presence tracking to see who's online
- Set up PubSub for distributed messaging
- Configure WebSocket transport options including
on_connectauth