Skip to content

PubSub & Distribution

beryl's PubSub layer is built on Erlang's built-in pg module (process groups). When a process subscribes to a topic, it joins a named pg group scoped to the PubSub instance. When a broadcast is sent, beryl looks up all members of that group and delivers the message to each one.

Because pg is cluster-aware, this works transparently across nodes in an Erlang cluster — a process on Node A subscribing to "room:lobby" will receive broadcasts from Node B without any additional configuration.

Each PubSub instance is isolated by a scope (an Erlang atom). The default scope is beryl_pubsub; use config_with_scope/1 to create isolated namespaces.

The Gleam module beryl/pubsub delegates all low-level pg operations to packages/beryl/src/beryl_pubsub_ffi.erl via @external declarations. The FFI file is intentionally minimal — it is a thin wrapper that maps Gleam calls directly to pg BIFs.

Public surface of beryl/pubsub:

FunctionDescription
start(config)Start a pg scope (idempotent — safe to call multiple times)
subscribe(ps, topic)Subscribe the calling process to a topic
unsubscribe(ps, topic)Remove the calling process from a topic
broadcast(ps, topic, event, payload)Deliver to all subscribers on all nodes
broadcast_from(ps, from, topic, event, payload)Deliver to all subscribers except from pid
broadcast_from_socket(ps, from, except_socket_id, topic, event, payload)Deliver to all subscribers except from, carrying a socket exclusion hint
local_broadcast(ps, topic, event, payload)Deliver to local-node subscribers only
subscribers(ps, topic)Return all subscriber pids (all nodes)
subscriber_count(ps, topic)Return subscriber count (all nodes)

The PubSubFrom type tags each message with its origin so downstream receivers can inspect whether a message came from the system, a specific process, or a process with an associated socket:

pub type PubSubFrom {
System
FromPid(Pid)
FromSocket(Pid, String)
}

broadcast_from and broadcast_from_socket implement sender exclusion: the originating process does not receive its own broadcast. This prevents a channel coordinator from echoing a message back to the socket that sent it.

  • broadcast_from(ps, from, ...) — skips delivery to the process whose Pid matches from.
  • broadcast_from_socket(ps, from, except_socket_id, ...) — also skips delivery to from, and carries FromSocket(from, except_socket_id) in the message so that any remote coordinator receiving it can optionally suppress re-delivery to a matching socket ID on their node.
flowchart LR
  subgraph Node1
    A[socket A] --- C1[coordinator]
  end
  subgraph Node2
    B[socket B] --- C2[coordinator]
  end
  C1 -- pg broadcast --> PG((pg group: topic))
  C2 -- subscribe --> PG
  PG -- deliver --> C2

When socket A sends a message on Node 1, its coordinator calls broadcast_from, which iterates the pg group members. Members on Node 2 receive the message via Erlang distribution — no extra message-bus infrastructure is required.

All traffic arriving over Erlang distribution is treated as fully trusted cluster input. There is no additional authentication layer between nodes: the Erlang cookie and network controls are the security boundary.

A process on any peer node can:

  • Subscribe to any pg group (PubSub topic) and receive all broadcasts.
  • Inject messages that downstream coordinators will process as legitimate internal traffic.
  • Inject reserved presence sync traffic, delivering false presence state to subscribers on all nodes. Ordinary WebSocket clients cannot reach these reserved internal topics — this vector is exclusive to trusted cluster peers.

Channel-level authorization — the join and handle_in callbacks — applies only to inbound WebSocket frames. It does not screen messages that arrive via distribution.

Refer to the Production Hardening guide for the full cluster security requirements (cookie strength, TLS distribution, EPMD port restrictions, and cluster isolation).

FileRole
packages/beryl/src/beryl/pubsub.gleamPublic Gleam API — types, config, and all broadcast functions
packages/beryl/src/beryl_pubsub_ffi.erlErlang FFI — thin pg wrappers called via @external