pre-1.0 · no Hex release yet
Phoenix channel
wire frames,
nothing more.
roost is a cross-target Gleam package that encodes and decodes the Phoenix channel text and binary wire protocol on Erlang and JavaScript. It owns no sockets, no channel processes, no reconnect loops — just the frames. You bring the transport; roost handles the protocol.
The roost logo: an orange bird settling into a woven slate nest.
// scope, on purpose
The protocol line,
drawn clearly.
roost handles the Phoenix channel wire format: encode the frames you send, decode the frames you receive, and give reserved events names. Sockets, processes, refs, reconnects, scheduling, and Presence stay in your application.
- Encode outbound frames Encode typed frames as Phoenix text or binary data, with explicit client/server direction.
- Decode inbound frames Parse socket text or bytes into
Push,Reply, orBroadcastvalues. - Reserved-event constants
phx_join,phx_leave,phx_reply,phx_error,phx_close,heartbeat. - Heartbeat & reply helpers Construct heartbeat frames, correlate join replies, and interpret reply statuses.
- Reply statuses & error types Typed reply statuses, encode/decode errors, join-reply correlation, and status interpretation instead of stringly-typed guesswork.
- Opening & closing sockets roost never touches a WebSocket or chooses its opcode. Use Mist, a browser WebSocket, or any transport.
- Channel processes & lifecycle Join / leave state machines and supervision live in your runtime.
- Ref counters & join refs Refs are plain strings on the wire; the monotonic counter is yours.
- Reconnects, rejoins & timeouts Backoff, retry, and rejoin policy are application decisions.
- Heartbeat scheduling & Presence roost builds the frame; when to send it is up to you.
// show, don't tell
Typed both ways.
encode turns a typed frame into Phoenix text or bytes.
decode turns socket data back into the same frame model —
or an explicit, typed error.
- One model for JSON and binary payloads
- Direction keeps Phoenix binary headers unambiguous
- Encode and decode failures are typed values
- The same protocol API on Erlang and JavaScript
import gleam/json
import gleam/option.{Some}
import roost
import roost/frame
/// Encode a phx_join frame for "room:lobby".
pub fn join() {
let message = frame.Push(
join_ref: Some("1"),
ref: Some("1"),
topic: "room:lobby",
event: "phx_join",
payload: frame.JsonPayload(
json.object([#("name", json.string("alice"))]),
),
)
roost.encode(message, direction: frame.ClientToServer)
// => Ok(frame.TextData("[\"1\",\"1\",..."))
}
/// BinaryPayload selects Phoenix's binary serializer.
pub fn upload(bytes: BitArray) {
frame.Push(
join_ref: Some("1"),
ref: Some("2"),
topic: "room:lobby",
event: "upload",
payload: frame.BinaryPayload(bytes),
)
|> roost.encode(direction: frame.ClientToServer)
// => Ok(frame.BinaryData(...))
}
/// Decode whatever the socket hands you into a typed value.
pub fn handle(data: frame.WireData) {
case roost.decode(data, direction: frame.ServerToClient) {
Ok(frame.Push(topic:, event:, ..)) -> route(topic, event)
Ok(frame.Reply(ref:, status:, ..)) -> handle_reply(ref, status)
Ok(frame.Broadcast(topic:, event:, ..)) -> route(topic, event)
Error(frame.InvalidJson(reason)) -> log(reason)
Error(frame.InvalidFormat(reason)) -> log(reason)
Error(frame.InvalidBinary(reason)) -> log(reason)
}
} Tiny protocol inspector
Text arrays. Binary headers.
Text frames use the familiar five-slot array shown below. Binary frames use Phoenix's compact direction-specific kind 0/1/2 headers followed by raw payload bytes; roost maps both into the same typed frame model.
// three steps, no ceremony
Settle in.
-
Add a Git dependency
roost will not be published on Hex until 1.0. Add it from Git and pin ref to the commit, tag, or branch you are testing.
[dependencies] roost = { git = "https://github.com/tylerbutler/roost.git", ref = "main" } -
Import & encode
Build a typed frame, encode it in the correct direction, and hand the resulting text or bytes to your socket.
import gleam/option.{Some} import roost import roost/frame let message = frame.Push( join_ref: Some("1"), ref: Some("1"), topic: "room:lobby", event: "phx_join", payload: frame.JsonPayload(payload), ) roost.encode(message, direction: frame.ClientToServer) -
Decode what comes back
Wrap the socket payload as TextData or BinaryData, then decode it into the same typed frame model.
case roost.decode(data, direction: frame.ServerToClient) { Ok(incoming) -> handle(incoming) Error(err) -> log(err) }
Still pre-1.0: install from Git, pin a ref, and expect API changes until the stable release.
Read the source on GitHub