Skip to content

Latest commit

 

History

History

showcase

README.md

beryl examples showcase

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 (one beryl.Sockets app with a beryl/channel handler per topic namespace: cursor:*, room:*, and document:)

Channels

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.

Tests

cd examples/showcase
gleam test      # channel behavior over the transport SPI
pnpm test       # Playwright end-to-end against a running server

Routing

Each 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.

Run locally

cd examples/showcase
gleam run
# then open http://localhost:8000

Honors PORT and BIND_ADDRESS like the standalone cursors demo.

Run in Docker

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-showcase

Deploy to Railway

Point 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.

Deploy to Fly.io

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.

  1. Install the Fly CLI if needed, then sign in:

    fly auth login
  2. Choose a globally unique app name. Replace beryl-showcase-yourname below, 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=false

    Select 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.

  3. 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=false on 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-only builds with your local Docker daemon instead of a remote builder.

  4. Check the deployment:

    fly status --config examples/showcase/fly.toml
    curl --fail "https://${APP_NAME}.fly.dev/healthz"

    The health endpoint returns ok. Open https://YOUR_APP_NAME.fly.dev/ and try /cursors and /chat in two browser tabs. Cursor movement and chat messages should appear in both tabs. The /docs HTTP route is currently disabled in the showcase router.

For startup failures, inspect the logs:

fly logs --config examples/showcase/fly.toml --no-tail

Idle shutdown and cost

The 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.