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.

roost handles 5 things
  • 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, or Broadcast values.
  • 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.
you bring your runtime
  • 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
channel.gleam
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.

Join a channel [join_ref, ref, topic, event, payload]

// three steps, no ceremony

Settle in.

  1. 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" }
  2. 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)
  3. 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