Architecture Overview
beryl is organized into several layers, each building on the one below it.
Layer diagram
Section titled “Layer diagram”┌─────────────────────────────────────────┐│ WebSocket Transport ││ (beryl/transport/mist) │├─────────────────────────────────────────┤│ Wire Protocol ││ (beryl/wire) │├─────────────┬───────────────────────────┤│ Channels │ Presence │ Groups ││ (beryl/ │ (beryl/ │ (beryl/ ││ channel) │ presence) │ group) │├─────────────┴───────────────┴───────────┤│ Coordinator (OTP actor) ││ (beryl/coordinator) │├─────────────────────────────────────────┤│ PubSub (pg) ││ (beryl/pubsub) │└─────────────────────────────────────────┘Core components
Section titled “Core components”PubSub (beryl/pubsub)
Section titled “PubSub (beryl/pubsub)”The foundation layer. Uses Erlang's pg module for distributed process groups. Processes subscribe to topics and receive broadcast messages. Works across Erlang cluster nodes automatically.
let assert Ok(ps) = pubsub.start(pubsub.default_config())pubsub.subscribe(ps, "room:lobby")pubsub.broadcast(ps, "room:lobby", "event", payload)Coordinator (beryl/coordinator)
Section titled “Coordinator (beryl/coordinator)”The central OTP actor managing all channel state:
- Handler registry — Maps topic patterns to channel handlers
- Socket tracking — Tracks connected sockets, their send functions, and subscribed topics
- Topic subscriptions — Maps topics to sets of subscriber socket IDs
- Message routing — Decodes wire protocol messages and dispatches to handlers
- Heartbeat enforcement — Periodic timer evicts sockets that miss heartbeats
The coordinator uses type erasure to store handlers with different assigns types in a single registry.
Channels (beryl/channel)
Section titled “Channels (beryl/channel)”The user-facing API for defining message handlers. Channels are built with a builder pattern:
channel.new(join_handler)|> channel.with_handle_in(message_handler)|> channel.with_handle_binary(binary_handler)|> channel.with_terminate(cleanup_handler)Each channel is parameterized by an assigns type that provides compile-time safety for per-socket state.
Presence (beryl/presence, beryl/presence/state)
Section titled “Presence (beryl/presence, beryl/presence/state)”Two-layer design:
-
beryl/presence/state— Pure CRDT: add-wins observed-remove set with causal context (vector clocks + cloud sets). Supportsjoin,leave,merge,compact,replica_up/down, and query operations. No side effects. -
beryl/presence— OTP actor wrapping the CRDT. Handles track/untrack calls, periodically broadcasts state via PubSub for cross-node replication, and fireson_diffcallbacks when merges produce changes.
Groups (beryl/group)
Section titled “Groups (beryl/group)”Named collections of topics managed by an OTP actor:
let assert Ok(groups) = group.start()let assert Ok(Nil) = group.create(groups, "team:eng")let assert Ok(Nil) = group.add(groups, "team:eng", "room:frontend")let assert Ok(Nil) = group.add(groups, "team:eng", "room:backend")
// Broadcast to all topics in the groupgroup.broadcast(groups, channels, "team:eng", "announce", payload)Supervisor (beryl/supervisor)
Section titled “Supervisor (beryl/supervisor)”Optional OTP supervision tree for all beryl subsystems:
import beryl/supervisorimport gleam/option.{None, Some}
let config = supervisor.SupervisedConfig( channels: beryl.default_config(), presence: Some(presence.default_config("node1")), groups: True,)let assert Ok(supervised) = supervisor.start(config)// supervised.channels, supervised.presence, supervised.groupsUses rest-for-one strategy with the child order: coordinator → presence → groups. A coordinator crash restarts all downstream children to maintain consistency. child_spec/1 allows embedding the beryl subtree inside a larger application supervisor.
Wire Protocol (beryl/wire)
Section titled “Wire Protocol (beryl/wire)”JSON encoding and decoding for the Phoenix-compatible wire protocol. Handles:
- Message parsing:
[join_ref, ref, topic, event, payload]arrays - Reply encoding with status (
ok/error) and response payload - Server push messages (no ref)
- Heartbeat replies
- Dynamic-to-JSON conversion for payloads
WebSocket Transport (beryl/transport/mist)
Section titled “WebSocket Transport (beryl/transport/mist)”Integrates directly with Mist to handle WebSocket connections:
- Generates a unique socket ID per connection
- Registers the socket's send function with the coordinator
- Routes incoming text frames through the wire protocol decoder
- Routes binary frames directly to the coordinator
- Notifies the coordinator on connection close
Topic (beryl/topic)
Section titled “Topic (beryl/topic)”Topic pattern matching for channel routing:
- Exact —
"room:lobby"matches only"room:lobby" - Wildcard —
"room:*"matches any topic starting with"room:" - Utilities:
segments,namespace,from_segments,validate,extract_id
Socket (beryl/socket)
Section titled “Socket (beryl/socket)”Opaque type representing a connected client with typed state:
id(socket)— Get the socket IDget_assigns(socket)/set_assigns(socket, assigns)— Typed per-socket statemap_assigns(socket, fn)— Transform assigns to a different type- Internal: transport access, metadata storage