Skip to content

Reference

The canonical function-level API reference is the generated Gleam documentation hosted on HexDocs:

https://hexdocs.pm/beryl/

This page provides a module map, broadcast cheatsheet, Phoenix wire protocol reference, and client compatibility notes.


ModuleWhat it doesWhen to use it
berylTop-level API: start the registry, register channels, broadcastEntry point for all applications
beryl/channelChannel builder, callback types, HandleResultDefining channel behaviour
beryl/coordinatorOTP actor that owns a single socket/topic pairRarely needed directly
beryl/socketSocket abstraction, assigns helpersInside channel callbacks
beryl/topicTopic parsing, wildcard matching, segment extractionDynamic routing, multi-tenant patterns
beryl/pubsubDistributed PubSub backed by Erlang pgMulti-node fan-out, cluster broadcasts
beryl/presenceOTP actor wrapping the presence CRDTTracking who is online
beryl/presence/statePure add-wins observed-remove CRDTTesting, custom merge logic
beryl/groupNamed sets of topics for bulk broadcastRooms with multiple sub-topics
beryl/wireJSON encode/decode for the Phoenix wire formatCustom transports, protocol debugging
beryl/transport/mistMist WebSocket upgrade and dispatchWiring beryl to an HTTP server

GoalAPINotes
Reply to an incoming messagechannel.Reply(event, payload, socket) from handle_inSends phx_reply; the event arg is ignored on the wire — reply is keyed by ref
Push to the current socket onlychannel.Push(event, payload, socket) from handle_in or handle_infoServer-originated push on this socket's topic
No responsechannel.NoReply(socket)Use when the handler has no output
Broadcast to all sockets on a topicberyl.broadcast(registry, topic, event, payload)All subscribers including the sender
Broadcast, excluding senderberyl.broadcast_from(channels, socket.id(socket), topic, event, payload)Second arg is except_socket_id: String; use socket.id/1 to extract it when you have a Socket value. Skips the originating socket; works across PubSub nodes
Send an OTP message to a channel actorberyl.send_info(channels, socket_id, topic_name, message)Delivers to handle_info; the callback receives a Dynamic value — decode and validate it there; no compile-time type checking
Broadcast presence diffberyl.broadcast_presence_diff(registry, topic, diff)Encodes Phoenix-shaped joins/leaves; only named topic entries are included

beryl speaks the same JSON array wire format as Phoenix channels. All frames are JSON arrays with five elements:

[join_ref, ref, topic, event, payload]
FieldTypeDescription
join_refstring or nullReference from the original phx_join frame; null for server-initiated pushes
refstring or nullPer-message reference echoed in the reply; null for pushes
topicstringThe channel topic, e.g. "room:lobby"
eventstringEvent name
payloadobjectArbitrary JSON object
EventDirectionMeaning
phx_joinclient → serverRequest to join a topic
phx_leaveclient → serverUnsubscribe from a topic
phx_replyserver → clientReply to a client message
phx_errorserver → clientJoin rejected or channel error
phx_closeserver → clientChannel closed by server
heartbeatclient → serverKeep-alive ping (topic "phoenix")

Sent in response to any client message. The event arg passed to channel.Reply is not reflected on the wire — the frame always uses phx_reply and the original ref.

[join_ref, original_ref, "topic:name", "phx_reply", {"status": "ok", "response": <your_payload>}]

A join reply uses the join_ref as both join_ref and ref:

["1", "1", "room:lobby", "phx_reply", {"status": "ok", "response": {}}]

The client sends heartbeats on the "phoenix" topic; beryl replies immediately:

// client →
[null, "ref", "phoenix", "heartbeat", {}]
// server →
[null, "ref", "phoenix", "phx_reply", {"status": "ok", "response": {}}]

Follows the Phoenix presence diff format. Both joins and leaves are objects keyed by presence key (typically the user ID). Each value has a metas array:

{
"joins": {
"user:42": { "metas": [{ "phx_ref": "abc123", "online_at": 1234567890 }] }
},
"leaves": {
"user:99": { "metas": [{ "phx_ref": "xyz789" }] }
}
}

broadcast_presence_diff encodes only named topic entries (entries with an explicit key). Anonymous entries are excluded.


Because beryl uses the standard Phoenix wire format, any Phoenix-compatible WebSocket client works out of the box:

ClientNotes
phoenix.jsOfficial JS client; full support
phxGleam client; designed for beryl
Phoenix Swift / Kotlin clientsCommunity Phoenix clients; wire-compatible
Plain WebSocketUse the JSON array format directly; no reconnect logic

The WebSocket upgrade path is caller-provided — there is no default. Pass the path when constructing your transport config with mist_transport.default_config(path). The Phoenix JS client appends /websocket to the socket endpoint, so if you configure the client with "/socket", mount your handler at "/socket/websocket". See the WebSocket Transport guide for details.


beryl follows Semantic Versioning but is not yet 1.0. Until the 1.0 release:

  • Minor version bumps (0.x → 0.x+1) may include breaking changes to the public API.
  • Patch version bumps (0.x.y → 0.x.y+1) fix bugs without intentional breakage.
  • Public API is defined as the exports of the modules listed in the module map above.
  • Lower-level modules (e.g. beryl/coordinator, beryl/wire) are public and usable, but are less stable than the top-level API and are primarily intended for advanced integrations or custom transports. Their signatures may change without a deprecation cycle.

Check GitHub releases and the Migration & Releases page before upgrading to a new minor version.