Skip to content

Groups

Groups are server-side named collections of topics. They let you broadcast a single event to many topics at once without tracking subscriptions yourself — similar to Socket.IO rooms or SignalR groups, but adapted to beryl's topic/channel model.

import beryl/group
let assert Ok(groups) = group.start()

group.start() returns Result(Groups, GroupError). The only failure case is StartFailed — an OTP actor spawn failure.

// Create a group
let assert Ok(Nil) = group.create(groups, "team:engineering")
// Error: AlreadyExists if the name is taken
case group.create(groups, "team:engineering") {
Ok(Nil) -> Nil
Error(group.AlreadyExists) -> Nil // already there
Error(_) -> Nil
}
// Delete a group (removes it and all its topic memberships)
let assert Ok(Nil) = group.delete(groups, "team:engineering")
// Error: NotFound if it doesn't exist
case group.delete(groups, "team:gone") {
Ok(Nil) -> Nil
Error(group.NotFound) -> Nil
Error(_) -> Nil
}

Topics are plain strings that match existing channel topics. Groups do not validate that a topic has any subscribers — they are just sets of strings.

let assert Ok(Nil) = group.add(groups, "team:engineering", "room:frontend")
let assert Ok(Nil) = group.add(groups, "team:engineering", "room:backend")
let assert Ok(Nil) = group.add(groups, "team:engineering", "room:infra")
// Remove one topic
let assert Ok(Nil) = group.remove(groups, "team:engineering", "room:infra")
// Both add and remove return Error(NotFound) if the group doesn't exist

Adding the same topic twice is a no-op (topics are stored in a set).

// List all topics in a group
case group.topics(groups, "team:engineering") {
Ok(topic_set) -> set.to_list(topic_set) // ["room:frontend", "room:backend"]
Error(group.NotFound) -> []
Error(_) -> []
}
// List all group names
let names = group.list_groups(groups) // ["team:engineering", "team:design"]

group.broadcast sends an event to every topic in the named group using beryl.broadcast internally. It is fire-and-forget: the return type is Nil, not Result. If the named group does not exist, the call silently does nothing.

group.broadcast(
groups,
channels,
"team:engineering",
"deploy_started",
json.object([#("env", json.string("production"))]),
)

This is equivalent to calling beryl.broadcast on each topic in the group in sequence.

ErrorWhen
AlreadyExistscreate called for a name already in use
NotFounddelete, add, remove, or topics called for an unknown group name
StartFailedgroup.start() — the OTP actor failed to initialize
import beryl
import beryl/group
import gleam/json
// At startup
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")
// Later: broadcast deployment notice to all engineering rooms
group.broadcast(
groups,
channels,
"team:eng",
"deploy_complete",
json.object([
#("version", json.string("1.4.2")),
#("deployed_by", json.string("ci")),
]),
)
// When a team is disbanded
let assert Ok(Nil) = group.delete(groups, "team:eng")

When using beryl/supervisor, set groups: True in SupervisedConfig and access the groups handle from the returned SupervisedChannels:

import beryl/supervisor
import gleam/option.{None}
let config = supervisor.SupervisedConfig(
channels: beryl.default_config(),
presence: None,
groups: True,
)
let assert Ok(supervised) = supervisor.start(config)
// supervised.groups is Option(group.Groups)
case supervised.groups {
Some(g) -> group.broadcast(g, supervised.channels, "team:eng", "alert", payload)
None -> Nil
}

See the Supervision guide for details on the supervised startup pattern.