Brazier
Brazier is an alpha full-stack Gleam framework for building MVU web apps on the
BEAM, published on Hex as brazier (gleam add brazier). It was named Beacon
until 2026-11-04 and was renamed because another package holds that name on
Hex; see
CHANGELOG.md.
How a page works:
- The server renders the first page (SSR).
- The browser loads a generated client bundle and connects over a WebSocket, one BEAM process per session.
- The server is authoritative:
updateruns there, and the syncedModelreaches the browser as JSON state updates. - An optional Local layer handles per-tab state such as dropdowns and form drafts in the browser, without a server round trip.
- With
brazier.full_application, server state thatsyncdoes not return stays on the BEAM and is never sent to the browser.
Minimal App
import brazier
import brazier/config
import brazier/element.{type Element}
import brazier/event
import brazier/html
import gleam/int
pub type Model {
Model(count: Int)
}
pub type Msg {
Increment
}
fn init(_ctx: brazier.Context) -> Result(Model, brazier.InitError) {
Ok(Model(count: 0))
}
pub fn update(model: Model, msg: Msg) -> Model {
case msg {
Increment -> Model(count: model.count + 1)
}
}
pub fn view(model: Model) -> Element(Msg) {
html.button([event.on_click(Increment)], [
html.text("Count: " <> int.to_string(model.count)),
])
}
pub fn main() {
let assert Ok(secret_key) = config.secret_key()
as "SECRET_KEY must be set to a private key of at least 32 bytes (config.secret_key logs why it was refused)"
let assert Ok(Nil) =
brazier.simple(init:, update:, view:)
|> brazier.secret_key(secret_key)
|> brazier.start(8080)
as "brazier.start failed (it logs the error)"
}
Every app needs a secret key of at least 32 bytes. It signs session and join
tokens and keys the cluster peer check. brazier.start refuses an app
without one and names the setting, and there is no generated default.
config.secret_key() reads it from SECRET_KEY.
An app needs two generated artifacts before it starts: the server codec
(src/brazier_codec.gleam) and the client bundle (priv/static/). Generate
them with gleam run -m brazier/build, then gleam run; or use
gleam run -m brazier/dev, which builds, runs and rebuilds on change.
brazier.start logs and returns an error when either artifact is missing,
and main crashes on it, so gleam run exits with status 1 and says what to
run. The examples do not track their generated files, so build one before its
first run. To run the counter example from this repository:
cd examples/counter
export SECRET_KEY="$(openssl rand -base64 48)" # once per shell
gleam run -m brazier/build # first, and after every change
gleam run
Keep the same SECRET_KEY across restarts: a new key invalidates every
signed token, so open tabs lose their session.
Getting Started walks through a full app.
Recommended APIs
- Pick one constructor:
brazier.simple(a pureupdatethat also runs in the browser),brazier.application(serverupdatewith effects) orbrazier.full_application(private server state,syncand a Local layer). brazier.with_router(parse:, on_change:)routes by URL; see Routing.brazier/api.routes,api.json_value,api.read_textandapi.read_formfor API routes.brazier/auth.session_routes,auth.protect_ws(orauth.allow_guests_ws) andauth.init_from_sessionfor session-cookie auth.
Requirements
- Gleam 1.19.2 and Erlang/OTP 28.
nodeandnpx: the build bundles the client with a pinned esbuild throughnpx, andgleam testneeds both (client round-trip tests run innode, and the build fixture tests bundle with esbuild). A missing tool fails those tests; they are never skipped.- For the browser (CDP) tests: Python 3 with
requirements-dev.txtin.venv, and Chrome or Chromium. See Testing.
Development
In the repository root (the framework itself has no main; run an app from
examples/):
gleam build # zero warnings
gleam format --check src test
gleam run -m brazier/lint # 0 violations
gleam test
scripts/check_docs.sh # doc samples compile, doc links resolve
All of them must pass before a change is done.
Project Docs
License
MIT; see LICENSE.