pre-0.1 · API unstable
Phoenix channel
wire frames,
nothing more.
roost is a pure Gleam package that encodes and decodes the Phoenix channel wire protocol. It owns no sockets, no channel processes, no reconnect loops — just the frames. You bring the transport; roost handles the protocol.
gleam add roost The roost logo: an orange bird settling into a woven slate nest.
// the boundary is the feature
roost does one thing.
It does it completely.
Most of evaluating a small library is finding out where it stops. So here's the line, drawn on purpose — what roost owns, and what stays yours.
- Encode outbound frames Build the Phoenix wire array
[join_ref, ref, topic, event, payload]from typed Gleam values. - Decode inbound frames Parse raw socket text back into a typed
Incomingvalue, with explicitDecodeErrors. - Reserved-event constants
phx_join,phx_leave,phx_reply,phx_error,phx_close,heartbeat. - Heartbeat & reply helpers Construct heartbeat frames on the
"phoenix"topic and well-formed reply frames. - Reply statuses & error types Typed reply statuses and decode errors instead of stringly-typed guesswork.
- Opening & closing sockets roost never touches a WebSocket. Use Mist, Gleam JS, 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 labelled Gleam arguments into a Phoenix wire
string. decode turns a raw socket message into an
Incoming value — or an explicit, typed error. No
stringly-typed payloads, no surprises.
- Labelled args mirror the wire array order
- Errors are values:
InvalidJson/InvalidFormat - Same shape compiles to 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() -> String {
roost.encode(
join_ref: Some("1"),
ref: Some("1"),
topic: "room:lobby",
event: "phx_join",
payload: json.object([#("name", json.string("alice"))]),
)
// => ["1","1","room:lobby","phx_join",{"name":"alice"}]
}
/// Decode whatever the socket hands you into a typed value.
pub fn handle(text: String) {
case roost.decode(text) {
Ok(frame.Incoming(topic:, event:, ..)) -> route(topic, event)
Error(frame.InvalidJson(reason)) -> log(reason)
Error(frame.InvalidFormat(reason)) -> log(reason)
}
} // the whole protocol, on one line
Five slots. That's the wire.
Every Phoenix channel frame is a five-element JSON array. roost encodes to it and decodes from it — so it helps to see exactly what each slot means. Hover or focus a slot to read its role.
- 0 join_ref
"1"Option(String) Ties a message to a specific channel join. Null until the channel is joined. - 1 ref
"1"Option(String) Per-message reference used to match a reply back to its request. - 2 topic
"room:lobby"String The channel topic the frame belongs to, e.g. a room or stream. - 3 event
"phx_join"String Event name — a reserved system event or one of your own. - 4 payload
{ … }json.Json The JSON body. Anything your application needs to send.
heartbeat frames are the same shape with
topic "phoenix" and a null join_ref — roost ships
a helper so you never hand-write one.
// three steps, no ceremony
Settle in.
-
Add the package
Pull roost into your Gleam project. No transitive runtime — it depends only on the standard library and gleam_json.
gleam add roost -
Import & encode
Build a frame with labelled arguments and hand the string to whatever socket you already use.
import roost roost.encode( join_ref: Some("1"), ref: Some("1"), topic: "room:lobby", event: "phx_join", payload: payload, ) -
Decode what comes back
Turn inbound socket text into a typed Incoming value, and handle InvalidJson / InvalidFormat as ordinary results.
case roost.decode(text) { Ok(incoming) -> handle(incoming) Error(err) -> log(err) }
Pre-0.1 and honest about it — the API can still move. Watch the repo to track it.
Read the source on GitHub