A single Gleam app that bundles the three example demos behind a landing page so they can be deployed together as one Fly.io or Railway service:
/— landing page with links to each demo/cursors— collaborative cursors/chat— chat rooms/docs— collaborative CRDT docs (HTTP route currently disabled)/healthz— health check/socket/websocket— shared WebSocket endpoint (oneberyl.Socketsapp with aberyl/channelhandler per topic namespace:cursor:*,room:*, anddocument:)
This is the multi-topic app the beryl/channel layer exists for. Each
namespace is a channel handler in src/showcase/channel/, registered as
one list in showcase.handlers; the layer routes every join, message, and
close to the handler that owns the topic, and each channel keeps its own
private state per joined topic. There is no socket-wide model, no message
union, and no hand-written router.
The standalone cursors, chatrooms, and collab_docs servers stay on
raw beryl.child_spec dispatch on purpose: each serves a single topic
namespace, which the core API already handles directly.
Session presence uses the same example-local ETS tracker as the standalone apps. Join and leave callbacks mutate it in their runtime turn, while its publisher broadcasts the current snapshot asynchronously. The room departure announcement remains a channel termination action.
The read-only lobby channel is mounted so the chat UI keeps receiving room
list invalidations. A room channel cannot target that other topic directly,
so the announcement goes through example_helper/broadcast_hub, a small actor holding the
beryl.Sockets handle — the equivalent of Phoenix's Endpoint.broadcast/3.
cd examples/showcase
gleam test # channel behavior over the transport SPI
pnpm test # Playwright end-to-end against a running serverEach example's router was refactored to accept a base_path field on its
Context. The standalone demos still pass "" and serve assets at
/static/.... The showcase passes "/cursors", "/chat", and "/docs"
respectively, so each example's HTML emits prefixed asset URLs and its
serve_static mount lives under the prefix. The Phoenix JS client in
each example uses new Socket("/socket") so all three share the same
WS endpoint without any client-side edits.
cd examples/showcase
gleam run
# then open http://localhost:8000Honors PORT and BIND_ADDRESS like the standalone cursors demo.
The Dockerfile expects the repo root as build context because the
project depends on beryl and the three example projects via relative
path dependencies.
# from the repository root
docker build -f examples/showcase/Dockerfile -t beryl-showcase .
docker run --rm -p 8000:8000 -e PORT=8000 beryl-showcasePoint the Railway service at this repo with:
- Root directory: repository root
- Config path:
examples/showcase/railway.toml
railway.toml selects examples/showcase/Dockerfile and a /healthz
health check.
fly.toml uses the same Dockerfile, one shared CPU, and 512 MB
of RAM in Ashburn, Virginia (iad). Fly terminates HTTPS and forwards HTTP
and WebSocket traffic to port 8000. The health check uses /healthz.
Run every command below from the repository root, not examples/showcase.
The Docker build needs the local packages and sibling examples.
The build file paths in fly.toml are relative to that config file; the build
context remains the repository root.
-
Install the Fly CLI if needed, then sign in:
fly auth login
-
Choose a globally unique app name. Replace
beryl-showcase-yournamebelow, then create the app without deploying it:APP_NAME=beryl-showcase-yourname fly launch --config examples/showcase/fly.toml --copy-config \ --name "$APP_NAME" --no-deploy --ha=falseSelect your organization. If prompted to review settings, keep the region and Machine size from the config. Do not add a database or other services; the demos do not need them. Launch updates the config's app name. The first deployment allocates the app's public addresses.
-
Start Docker locally, validate the config, and deploy:
fly config validate --config examples/showcase/fly.toml --strict fly deploy --config examples/showcase/fly.toml --local-only --ha=false
Keep
--ha=falseon later deployments too. It prevents Fly from creating a spare Machine. This showcase keeps its state in one BEAM instance and does not configure clustering; keep it on one Machine.--local-onlybuilds with your local Docker daemon instead of a remote builder. -
Check the deployment:
fly status --config examples/showcase/fly.toml curl --fail "https://${APP_NAME}.fly.dev/healthz"The health endpoint returns
ok. Openhttps://YOUR_APP_NAME.fly.dev/and try/cursorsand/chatin two browser tabs. Cursor movement and chat messages should appear in both tabs. The/docsHTTP route is currently disabled in the showcase router.
For startup failures, inspect the logs:
fly logs --config examples/showcase/fly.toml --no-tailThe Machine stops after an idle period and starts when a new request arrives. Open WebSocket connections can keep it running, even if nobody is interacting with the page. The first request after a stop can take longer.
Demo state is held in memory and resets when the Machine stops, restarts, or
is redeployed. No database or volume is provisioned. To keep one Machine
running between visits, set min_machines_running = 1 and deploy again.
Fly charges for running CPU and RAM, outbound traffic, and other billable resources. A stopped Machine still incurs root filesystem storage charges; idle shutdown does not make hosting free. See the current Fly pricing before comparing it with your Railway bill.
Keep Railway running until the Fly health check and two-tab demos work. After switching any links or custom domain to Fly, remove the old Railway service if you no longer need it, so it does not keep billing.