Skip to content

Architecture Overview

beryl layers a Phoenix-style channel system on top of OTP actors and Erlang pg, with a pluggable wire codec and WebSocket transport. Channels, presence, and groups are independent domain actors wired together by the coordinator, which dispatches decoded wire messages and enforces heartbeats. PubSub is the only cross-node primitive; everything else is local to a node.

Each subsystem page covers one slice of the stack end-to-end and ends with a "Where this lives" pointer back to the relevant source files.

flowchart TB
  T["WebSocket Transport<br/>beryl_mist · beryl_ewe"]
  W["Wire Protocol<br/>beryl/wire · beryl/wire/codec"]
  subgraph Domain["Channel domain"]
    C["Channels<br/>beryl/channel"]
    P["Presence<br/>beryl/presence"]
    G["Groups<br/>beryl/group"]
  end
  CO["Coordinator (OTP actor)<br/>beryl/coordinator"]
  PS["PubSub (Erlang pg)<br/>beryl/pubsub"]
  T --> W --> Domain --> CO --> PS
ModuleResponsibilityPage
berylPublic entry-point: config/1, register/3, broadcast/4, send_info/4. Starting is beryl/supervisor's job — beryl has no start
beryl/coordinatorCentral OTP actor: handler registry, socket tracking, message routing, heartbeat enforcementCoordinator
beryl/pubsubDistributed pub-sub via Erlang pg; subscribe, broadcast, and broadcast_fromPubSub & Distribution
beryl/presenceOTP actor wrapping an add-wins OR-set CRDT; track/untrack, cross-node diff broadcast, on_diff callbacksPresence
beryl/presence/wirePhoenix-compatible JSON encoding for presence diffs (joins/leaves maps)Presence
beryl/wirePluggable codec surface; ships phoenix_codec() for [join_ref, ref, topic, event, payload] framingWire & Transport
beryl/wire/codecCodec type contract: decode_text, decode_binary, encode_* — lets you swap framingWire & Transport
beryl_mistMist WebSocket adapter: assigns socket IDs, registers send functions, routes frames to coordinatorWire & Transport
beryl_eweEwe WebSocket adapter; mirrors the beryl_mist API against the same beryl/transport SPIWire & Transport
beryl/supervisorone-for-one supervision tree isolating an optional connection limiter from a nested rest-for-one channel subtree (registry → coordinator → presence → groups); returns a child specification via start/1Coordinator
beryl/groupNamed topic collections managed by an OTP actor; supports grouped broadcast
beryl/topicTopic pattern matching: exact strings, "ns:*" prefix wildcards, and segment wildcards ("document:*:ops")
beryl/socketOpaque connected-client type with typed assigns; id, get_assigns, set_assigns, map_assigns
beryl/channelBuilder API for user-defined message callbacks parameterized by an assigns typeMessage Lifecycle
beryl/errorOpaque StartFailure type that hides OTP's actor.StartError from public APIs
beryl/rate_limitToken-bucket rate limiter backed by an OTP registry actor; keyed by socket ID or topic
beryl/bridgeForwards an external OTP actor's message stream into a socket channel; avoids per-socket forwarder boilerplate
beryl/logInternal logging shim over palabres; thin named-logger surface, not public API
beryl/internalShared internal utilities (logging config, configure helper); not public API
flowchart TB
  S["beryl supervisor (one-for-one)"]
  S --> LI["connection limiter (optional)"]
  S --> CH["channel supervisor (rest-for-one)"]
  CH --> RE["registry"]
  CH --> CO["coordinator"]
  CH --> PR["presence (optional)"]
  CH --> GR["groups (optional)"]
  CO -. "crash restarts downstream" .-> PR
  PR -. .-> GR

beryl is a monorepo. Core library sources live under packages/beryl/src/, and each transport is its own package (packages/beryl_mist/src/, packages/beryl_ewe/src/). The coordinator and supervisor are the entry points for understanding runtime behaviour — start with Coordinator & Supervision. For how a message actually moves through the system, see Message Lifecycle. For cross-node concerns, see PubSub & Distribution.