Skip to content

Examples

beryl ships three fully runnable example applications in the examples/ directory. They use a Gleam/BEAM backend with browser frontends that exercise different realtime collaboration patterns.

Source: examples/cursors

Move your mouse and see everyone else's cursor in real time. Open the app in multiple browser tabs to try it.

Terminal window
cd examples/cursors
gleam run
# Open http://localhost:8000 in multiple browser tabs
beryl featureHow it's used
Channelscursor:lobby channel handles join/leave and cursor events
Topic wildcardsHandler registered on cursor:* matches any cursor room
Presence (CRDT)Tracks connected users with username + color metadata
broadcast_fromFans out cursor moves to all other clients, excluding the sender
Rate limitingberyl.with_message_rate throttles high-frequency cursor events
WebSocket transportmist_transport.upgrade handles Phoenix-compatible WebSocket requests
Phoenix JS clientFrontend uses the official phoenix package over the standard wire protocol
Browser (vanilla JS + Phoenix JS client)
│ WebSocket (Phoenix wire protocol)
Server (Gleam)
├── Mist HTTP — serves HTML + static files
├── beryl channels — cursor:* topic handler
├── beryl presence — CRDT-backed user tracking
└── beryl pubsub — broadcast_from cursor positions

Source: examples/chatrooms

A multi-room chat app with authentication, join rejection, typing indicators, and message acknowledgment.

Terminal window
cd examples/chatrooms
gleam run
# Open http://localhost:8001?token=beryl-demo in multiple browser tabs

This demo is designed to complement the cursors example by covering a different slice of the beryl API:

beryl featureHow it's used
on_connect authToken query param validated before WebSocket upgrade is accepted
JoinErrorRooms reject joins when full (20-user cap) or when room doesn't exist
channel.ReplyDelivery of new_msg confirmed with a msg_ack phx_reply
channel.error_with_codeEmpty messages rejected with HTTP-style code 422
GroupsThree rooms (general, random, help) organised in a named group
Presence (typing indicators)Typing state stored in presence meta; updated on typing/stop_typing events
System messages"user joined" / "user left" broadcast via beryl.broadcast on join/terminate
Multiple topicsEach room is a separate room:* topic sharing one registered channel handler
Rate limitingwith_join_rate (5/sec) and with_channel_rate (10/sec/channel)
Browser (vanilla JS + Phoenix JS client, token auth)
│ WebSocket (Phoenix wire protocol, ?token=beryl-demo)
Server (Gleam)
├── Mist HTTP — static files, /api/rooms
├── Mist WebSocket transport — on_connect validates token
├── beryl channels — room:* handler
├── beryl groups — "public" group → general, random, help
└── beryl presence — online users + typing indicators
DirectionEventPurpose
Client → Servernew_msgSend a chat message {text}
Client → ServertypingStart typing indicator
Client → Serverstop_typingStop typing indicator
Server → Clientnew_msgBroadcast message {text, username, color, type, timestamp}
Server → Clientphx_reply (push ref)Reply to a client push — used for both delivery acknowledgment (status: "ok") and validation errors (status: "error", response: {code, error}). Validation errors are returned via channel.Reply from handle_in, not pushed as a separate event.
Server → Clientpresence_listUpdated online user list
Server → ClienttypingTyping indicator update

Source: examples/collab_docs

A collaborative document editor where clients merge block state locally with a CRDT and use beryl as an unordered realtime transport.

Terminal window
just deps
cd examples/collab_docs && gleam run
# Open http://localhost:8002 in multiple browser tabs
beryl featureHow it's used
Segment wildcard topicsHandler registered on document:*:* isolates each tenant/document pair
Client-side CRDT mergeBrowser state uses lattice_core, lattice_maps, and lattice_registers with ORMap(MVRegister(String)) document blocks
Unordered realtime transportBeryl broadcasts document updates while the CRDT handles merge convergence
Late joiner cacheServer returns cached merged state in the join reply for new clients
Conflict resolution UIConcurrent edits to the same block render explicit conflict cards with all versions
Browser (vanilla JS + Phoenix JS client + lattice CRDT packages)
│ WebSocket (Phoenix wire protocol)
Server (Gleam)
├── Mist HTTP — serves HTML + static files
├── beryl channels — document:*:* handler
├── document cache — merged state for late joiners
└── beryl pubsub — fan-out of CRDT updates

Starting pointGo here
I want a minimal working example right nowQuick Start
I want to see live presence + cursorsexamples/cursors
I want auth, join validation, or groupsexamples/chatrooms
I want collaborative documents or CRDT conflictsexamples/collab_docs
I want to understand the channel APIChannels guide
I want to add presence to my appPresence guide