brazier
Types
An app being configured. Build one with simple, application or
full_application, pipe it through the options, then call start.
state: what the server holds per session.model: the client-visible part (sync(state)), sent to the browser.local: client-only state, never sent to the server.msg: the app’s messages.
pub opaque type App(state, model, local, msg)
What init knows about the request it runs for: the page URL, the
cookies, whether this is the connected mount (WebSocket) or the dead
render (HTTP), and the browser session.
init runs twice per page, like LiveView’s mount: once for the HTML
response (is_connected False, effects dropped) and once when the
browser connects (is_connected True, effects run).
pub type Context =
@internal Context
Why init refused to start a session. Build one with error.unavailable
(503) or error.refuse(status, reason) from brazier/error.
Every constructor’s init returns Result(_, InitError), like LiveView’s
mount returning {:ok, socket}. An Error is answered on the dead
render with an error page for its status, and on the connected mount by
closing the WebSocket with 4000 + status (4503 for unavailable). The
reason is logged and never sent to the browser. A crash in init is
answered with 500 on the dead render and closes the WebSocket with 1011.
pub type InitError =
error.InitError
Values
pub fn api_routes(
app: App(state, model, local, msg),
handler: fn(request.Request(server.Connection)) -> option.Option(
response.Response(server.ResponseBody),
),
) -> App(state, model, local, msg)
Register an API route handler. It runs before page rendering and static
files on every HTTP request: Some(response) answers the request, None
falls through.
Prefer brazier/api.routes with api.get, api.post and the response
helpers for ordinary JSON/text endpoints.
pub fn application(
init init: fn(Context) -> Result(
#(model, effect.Effect(msg)),
error.InitError,
),
update update: fn(model, msg) -> #(model, effect.Effect(msg)),
view view: fn(model) -> element.Element(msg),
) -> App(model, model, Nil, msg)
An app whose whole model is synced to the client. update runs only on
the server and returns effects.
pub fn broadcast(topic: String) -> Nil
Broadcast a notification to a topic. Every session subscribed to it
gets its on_notify message.
pub fn broadcast_payload(topic: String, payload: a) -> Nil
Broadcast a typed payload to a topic (read it in on_notification with
notification_payload).
pub fn cluster(
app: App(state, model, local, msg),
config: cluster.ClusterConfig,
) -> App(state, model, local, msg)
Join a cluster: the cluster manager (built with cluster.new) runs in
the application’s supervision tree right after PubSub, connecting to
and checking peers. Start fails when the configuration or this node’s
distribution does not pass cluster’s checks.
pub fn cookie(ctx: Context, name: String) -> Result(String, Nil)
A request cookie by name.
pub fn dev_mode(
app: App(state, model, local, msg),
enabled: Bool,
) -> App(state, model, local, msg)
Enable Brazier’s client dev tracing.
pub fn full_application(
init init: fn(Context) -> Result(
#(state, effect.Effect(msg)),
error.InitError,
),
update update: fn(state, msg) -> #(state, effect.Effect(msg)),
sync sync: fn(state) -> model,
local local: local.Layer(model, local, msg),
view view: fn(model, local) -> element.Element(msg),
) -> App(state, model, local, msg)
An app with all three state layers:
state, held only by the server (secrets, history, handles);- the Model,
sync(state), rendered byviewand sent to the browser; - Local, client-only state from the
locallayer (local.layerorlocal.none()).
Only the types sync, view and the Local layer reach are compiled for
the browser, so the rest of state never leaves the server.
pub fn head_html(
app: App(state, model, local, msg),
html: String,
) -> App(state, model, local, msg)
Extra HTML for the page’s <head> (stylesheets, meta tags, fonts).
pub fn is_connected(ctx: Context) -> Bool
True for the connected mount, False for the dead render (LiveView’s
connected?/1).
pub fn notification_payload(
notification: notification.Notification,
decoder: decode.Decoder(a),
) -> Result(a, List(decode.DecodeError))
Decode a notification’s payload.
pub fn on_notification(
app: App(state, model, local, msg),
handler: fn(notification.Notification) -> msg,
) -> App(state, model, local, msg)
The message for a typed notification on a subscribed topic.
pub fn on_notify(
app: App(state, model, local, msg),
handler: fn(String) -> msg,
) -> App(state, model, local, msg)
The message for a notification on a subscribed topic (by topic).
pub fn presence(
app: App(state, model, local, msg),
tracker tracker: presence.PresenceConfig(meta),
track track: fn(state) -> List(presence.Tracking(meta)),
on_presence on_presence: fn(sync.Diff(meta)) -> msg,
) -> App(state, model, local, msg)
Track who is here (Phoenix.Presence). The tracker (built with
presence.new) runs in the application’s supervision tree, and every
session tracks what track returns for its state: computed after every
update, tracked, updated and untracked as the list changes, and gone
when the session ends. Every change on a tracked topic, from any node,
arrives as on_presence(diff): the session’s first message for a topic
lists everyone already there as joins, and up to 100 queued changes are
coalesced into one message. Keep the entries with
brazier/presence/sync.merge. The diff, entry and meta types live in
brazier/presence/sync, which runs in the browser, so Msg and the
Model can carry them.
pub type Msg {
PresenceChanged(sync.Diff(Profile))
}
brazier.application(init:, update:, view:)
|> brazier.presence(
tracker: presence.new("online", encode_user, user_decoder()),
track: fn(model) {
[presence.Tracking(topic: "room:" <> model.room, key: model.user, meta: model.profile)]
},
on_presence: PresenceChanged,
)
A tracking the tracker refuses (a limit, an invalid topic or key, the same key twice on a topic) stops the session with a logged error. When a shard of the tracker restarts, every session tracks its entries on it again. Start fails when the tracker configuration is invalid.
pub fn pubsub_config(
app: App(state, model, local, msg),
config: pubsub.PubSubConfig,
) -> App(state, model, local, msg)
Configure PubSub (partitions, pool size, limits). Start from
pubsub.default_config(). PubSub runs as the first child of the
application’s supervision tree.
pub fn secret_key(
app: App(state, model, local, msg),
key: String,
) -> App(state, model, local, msg)
The app’s secret key, required: at least 32 bytes. It signs join and
session tokens, and the cluster peer check derives its key from it with
a purpose label, so every node of a cluster needs the same one. Read it
from the environment with config.secret_key() (SECRET_KEY). start
refuses an app without one: a generated key would invalidate every token
at each restart and make every node’s key different.
pub fn security_limits(
app: App(state, model, local, msg),
limits: transport.SecurityLimits,
) -> App(state, model, local, msg)
Override the transport’s security limits. Start from
transport.default_security_limits().
pub fn session_id(ctx: Context) -> Result(String, Nil)
An opaque, stable id for the browser session (a hash of Brazier’s session cookie), the same for the dead render and the connected mount. Error when the request has no Brazier session cookie (its first page load).
pub fn simple(
init init: fn(Context) -> Result(model, error.InitError),
update update: fn(model, msg) -> model,
view view: fn(model) -> element.Element(msg),
) -> App(model, model, Nil, msg)
A Lustre-style app with a pure update.
The build compiles update to JavaScript, so a click updates the page
at once. The message is still sent to the server, which runs the same
update and stays authoritative: when the two models differ, the
server’s model wins and the browser logs the mismatch.
update may only use code that can run in the browser (the same rule
as view); the build fails, naming the call, when it reaches anything
else. Use application for effects or server-only work.
pub fn start(
app: App(state, model, local, msg),
port: Int,
) -> Result(Nil, error.BrazierError)
Start the app on port under its supervision tree and block while it
runs (application.wait_forever). It returns Error when the app cannot
start, and Ok(Nil) once it has stopped: after SIGTERM, when the
listener has drained its connections, or when the tree was stopped
elsewhere. If the tree gives up (a child keeps crashing), the calling
process crashes with the reason instead of running on without it.
The server codec and the client bundle are build artifacts of
gleam run -m brazier/build (or gleam run -m brazier/dev); starting
without them is an error.
pub fn static_dir(
app: App(state, model, local, msg),
dir: String,
) -> App(state, model, local, msg)
Serve static files from dir under /static.
pub fn subscriptions(
app: App(state, model, local, msg),
compute: fn(state) -> List(String),
) -> App(state, model, local, msg)
Topics this session subscribes to, computed from the state after every update. Brazier diffs the list and subscribes or unsubscribes as needed.
brazier.application(init:, update:, view:)
|> brazier.subscriptions(fn(model) { ["room:" <> model.current_room] })
|> brazier.on_notify(RoomUpdated)
pub fn title(
app: App(state, model, local, msg),
title: fn(model) -> String,
) -> App(state, model, local, msg)
The page title for the client model. The server render puts it in the
document’s <title>; after that, the browser’s title follows live
updates: each join sends the current title, and so does every update
that changes it.
pub fn track_and_watch_presence(
app: App(state, model, local, msg),
tracker tracker: presence.PresenceConfig(meta),
track track: fn(state) -> List(presence.Tracking(meta)),
watch watch: fn(state) -> List(String),
on_presence on_presence: fn(sync.Diff(meta)) -> msg,
) -> App(state, model, local, msg)
presence and watch_presence together: every session tracks what
track returns for its state and also follows the topics watch
returns, without being on them. A chat room that shows who is in the
room and how many are in the lobby uses this. Both lists are computed
after every update. A topic that is both tracked and watched is followed
once, so its diffs arrive once. The diffs, limits and failures are those
of presence and watch_presence.
brazier.application(init:, update:, view:)
|> brazier.track_and_watch_presence(
tracker: presence.new("online", encode_user, user_decoder()),
track: fn(model) {
[presence.Tracking(topic: "room:" <> model.room, key: model.user, meta: model.profile)]
},
watch: fn(_model) { ["lobby"] },
on_presence: PresenceChanged,
)
It, presence and watch_presence set the same thing: the later call
replaces the earlier.
pub fn uri(ctx: Context) -> uri.Uri
The page URL (for the connected mount, the URL the browser shows).
pub fn watch_presence(
app: App(state, model, local, msg),
tracker tracker: presence.PresenceConfig(meta),
watch watch: fn(state) -> List(String),
on_presence on_presence: fn(sync.Diff(meta)) -> msg,
) -> App(state, model, local, msg)
Watch who is here without being there: every session follows the topics
watch returns for its state (computed after every update) and tracks
nothing. A dashboard or a lobby list uses this. The diffs are those of
presence: the first message for a topic lists everyone already there
as joins, up to 100 queued changes are coalesced into one message, every
entry is reported exactly once, and a topic the session stops watching
reports everyone it knew there as leaves.
brazier.application(init:, update:, view:)
|> brazier.watch_presence(
tracker: presence.new("online", encode_user, user_decoder()),
watch: fn(model) { ["room:" <> model.room] },
on_presence: PresenceChanged,
)
It, presence and track_and_watch_presence set the same thing: the
later call replaces the earlier. To track and watch, use
track_and_watch_presence. An invalid topic (empty, or over
presence.max_topic_bytes) fails the session’s start, or stops it
later, with a logged error.
Start fails when the tracker configuration is invalid.
pub fn with_middleware(
app: App(state, model, local, msg),
mw: fn(
request.Request(server.Connection),
fn(request.Request(server.Connection)) -> response.Response(
server.ResponseBody,
),
) -> response.Response(server.ResponseBody),
) -> App(state, model, local, msg)
Add a middleware to the HTTP pipeline (after the ones already added).
pub fn with_router(
app: App(state, model, local, msg),
parse parse: fn(uri.Uri) -> Result(route, Nil),
on_change on_change: fn(route) -> msg,
) -> App(state, model, local, msg)
Route the app by URL. Declare one Route type and a parse function
(usually a case over router.segments(uri)); Brazier runs
update(state, on_change(route)) right after init, before the first
render, and again on every client navigation (LiveView’s
handle_params). A URL parse rejects is answered with 404, and a URL
whose percent-encoding does not decode with 400.
pub fn with_state_recovery(
app: App(state, model, local, msg),
restore restore: fn(model, state) -> state,
) -> App(state, model, local, msg)
Recover the session after a reconnect to a new server process: the
page carries the Model in a signed join token. init (and the initial
route) run first for the new connection, as LiveView re-runs mount;
restore then gets the recovered Model and that fresh state and
returns the state to start from. Server-only fields the Model does not
carry (a user loaded from the session, a handle) come from the fresh
state, so they are never left empty. For simple and application,
where the state is the Model, pass fn(model, _fresh) { model }.
pub fn ws_auth(
app: App(state, model, local, msg),
auth: fn(request.Request(server.Connection)) -> Result(
Nil,
String,
),
) -> App(state, model, local, msg)
Authenticate WebSocket connections. Runs before the upgrade; Error
rejects the connection with 401.