Arizona is a real-time web framework for Erlang/OTP. It renders HTML on the server, diffs changes at the template level, and pushes minimal updates to the browser over WebSocket.
Templates are plain Erlang terms compiled via parse transform. The server owns the state; the client is thin -- a DOM patcher in the browser, and the same diff stream drives a JSON widget tree on native app clients (see the native target below).
Arizona is powered by Roadrunner, a pure-Erlang HTTP and WebSocket server. Beep beep.
Arizona is in 0.x. The core is functional and covered by tests, but the API may change between
minor versions. Pin an exact version in your deps (e.g. {arizona, "0.1.0"}) if you need stability
across upgrades.
- SSR + live updates -- HTML on first load, WebSocket diffs after
- Erlang-native templates --
{Tag, Attrs, Children}tuples compiled by parse transform - Compile-time static/dynamic split -- statics sent once, only dynamics cross the wire
- Two handler kinds --
arizona_stateful(live: route pages and components),arizona_stateless(pure templates) - Streams -- keyed collections with insert/delete/update/move/sort/limit
- SPA navigation --
az_navigatelinks, server renders the next page over WebSocket - PubSub -- cross-view, cross-tab messaging via
arizona_pubsub - Route middlewares -- gate or rewrite requests before mount (auth, sessions, URL projection)
- On-mount hooks -- per-route pipeline that runs before every mount, including navigate
- Element hooks -- client-side
mounted/updated/destroyedcallbacks viaaz_hook - Dev-mode hot reload --
fswatcher recompiles changed.erlfiles and pushes reload events - HTTP/WebSocket transport -- HTTP/1.1, HTTP/2, HTTP/3 (experimental), and WebSocket built in
- Native (JSON) render target -- the same templates and diff engine also emit a JSON widget tree
via
?nativefor non-browser clients. In-repo Android (Compose), iOS (SwiftUI), and JS reference clients consume the same wire, andarizona_user_agentlets one view dual-serve HTML or native byUser-Agent. See docs/native.md - Terminal (ANSI) render target -- the same templates and diff engine also render to an ANSI
terminal via
?terminal, served over a local TTY or SSH; a transport-agnostic session and a pluggable driver model the key map and paint. See docs/architecture.md
- Erlang/OTP 28+
Add Arizona to your rebar.config dependencies:
{deps, [
arizona
]}.To track unreleased changes, swap the version for a git ref:
{arizona, {git, "https://github.com/arizona-framework/arizona.git", {branch, "main"}}}The client JavaScript ships baked into the rebar3 build (priv/static/assets/js/*.min.js). If
you need to bundle it yourself or consume it from a non-Erlang backend, install via npm:
npm install @arizona-framework/clientimport { connect } from '@arizona-framework/client';
connect('/ws');A page with an embedded counter.
id and initial count come from the parent via ?stateful (step 2), so mount/1 just passes
them through:
%% src/my_counter.erl
-module(my_counter).
-include_lib("arizona/include/arizona_stateful.hrl").
-export([mount/1, render/1, handle_event/3]).
mount(Bindings) ->
{Bindings, #{}}.
render(Bindings) ->
?html(
%% id must be on the root element -- if it changes, the component is remounted
{'div', [{id, ?get(id)}], [
{button, [{az_click, arizona_js:push_event(~"dec")}], [~"-"]},
{span, [], [~" Count: ", ?get(count), ~" "]},
{button, [{az_click, arizona_js:push_event(~"inc")}], [~"+"]}
]}
).
handle_event(~"inc", _Payload, Bindings) ->
{Bindings#{count => maps:get(count, Bindings) + 1}, #{}, []};
handle_event(~"dec", _Payload, Bindings) ->
{Bindings#{count => maps:get(count, Bindings) - 1}, #{}, []}.?get(count) registers count as a dependency of that template slot. When handle_event returns
new bindings, only slots whose tracked keys changed re-render -- the <span> patches; the buttons
don't.
The tuples carry more than just bindings: mount/1 returns {Bindings, Resets} (an explicit
slot-reset map -- usually #{}), and handle_event/3 returns {Bindings, Resets, Effects} where
Effects is a list of arizona_js commands (set_title, navigate, …) executed on the client.
A route's root handler is a stateful handler. It receives initial bindings:
%% src/my_page.erl
-module(my_page).
-include_lib("arizona/include/arizona_stateful.hrl").
-export([mount/1, render/1]).
mount(Bindings) ->
{Bindings, #{}}.
render(Bindings) ->
?html(
{main, [{id, ?get(id)}], [
{h1, [], [~"Counter demo"]},
%% id is required -- it's how the diff engine routes patches to this component
?stateful(my_counter, #{id => ~"counter", count => 0})
]}
).The HTML shell. Loads the client runtime that connects over WebSocket:
%% src/my_layout.erl
-module(my_layout).
-include_lib("arizona/include/arizona_stateless.hrl").
-export([render/1]).
render(Bindings) ->
?html([
~"<!DOCTYPE html>",
{html, [], [
{head, [], [
{meta, [{charset, ~"utf-8"}]},
{title, [], [?get(title, ~"Arizona")]}
]},
{body, [], [
?inner_content,
{script, [{type, ~"module"}], [
~"""
import { connect } from '/assets/arizona.min.js';
connect('/ws');
"""
]}
]}
]}
]).Add arizona to your app's applications list in .app.src:
{applications, [kernel, stdlib, arizona]}Ensure rebar3 shell loads the config and starts your app by adding
to rebar.config:
{shell, [
{config, "config/sys.config"},
{apps, [yourapp]}
]}.Replace yourapp with your project's app name. Then declare routes in config/sys.config:
[{arizona, [
{server, #{
routes => [
{live, ~"/", my_page, #{
layouts => [{my_layout, render}],
bindings => #{id => ~"page", title => ~"Counter demo"}
}},
{ws, ~"/ws", #{}},
{asset, ~"/assets", {priv_dir, arizona, "static/assets/js"}}
]
}}
]}].rebar3 shellOpen http://localhost:4040 and click the buttons -- the server renders the initial HTML, then pushes minimal diffs over WebSocket as the count changes.
See docs/architecture.md for the full architecture reference -- module breakdown, op codes, dev-mode file watchers, custom schemes/proto_opts, and imperative startup.
See docs/native.md for the native (JSON widget-tree) render target -- authoring
?native views, serving HTML and native from one view, the client wire contract, and the in-repo
Android client.
Arizona is open source and maintained on personal time. If you or your company find it useful, consider sponsoring.
I also accept coffees ☕
Contributions are welcome! Please see CONTRIBUTING.md for development setup, testing guidelines, and contribution workflow.
Copyright (c) 2023-2026 William Fank Thomé
Arizona is open-source under the Apache 2.0 License on GitHub.
See LICENSE.md for more information.