Current section
Files
Jump to
Current section
Files
README.md
# Arizona
[](https://github.com/arizona-framework/arizona/actions/workflows/erlang.yml)
[](https://github.com/arizona-framework/arizona/actions/workflows/node-js.yml)
[](https://hex.pm/packages/arizona)
[](https://hexdocs.pm/arizona/)
[](https://www.npmjs.com/package/@arizona-framework/client)
[](https://github.com/arizona-framework/arizona/blob/main/LICENSE.md)

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 runs on the HTTP/WebSocket server you pick, through an adapter package:
[`arizona_roadrunner`](https://github.com/arizona-framework/arizona_roadrunner) for
[Roadrunner](https://github.com/arizona-framework/roadrunner), a pure-Erlang server (beep beep),
[`arizona_cowboy`](https://github.com/arizona-framework/arizona_cowboy) for Cowboy, or
[`arizona_nova`](https://github.com/arizona-framework/arizona_nova) inside a Nova application.
## 🚧 Status
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.
## Features
- **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_navigate` links, 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`/`destroyed` callbacks via `az_hook`
- **Dev-mode hot reload** -- a pluggable file watcher (`arizona_watcher_fs`, a dev-only dep)
recompiles changed `.erl` files and pushes reload events
- **Server adapters** -- Roadrunner (HTTP/1.1, HTTP/2, HTTP/3) or Cowboy (HTTP/1.1, HTTP/2), each
in its own package and held to one conformance suite; Arizona can also share a listener another
framework owns, as `arizona_nova` does for Nova
- **Native (JSON) render target** -- the same templates and diff engine also emit a JSON widget tree
via `?native` for non-browser clients. In-repo Android (Compose), iOS (SwiftUI), and JS
reference clients consume the same wire, and `arizona_user_agent` lets one view dual-serve HTML
or native by `User-Agent`. See [docs/native.md](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](docs/architecture.md)
## Requirements
- Erlang/OTP 29+
## Installation
Add Arizona and a server adapter to your `rebar.config` dependencies:
```erlang
{deps, [
arizona,
arizona_roadrunner
]}.
```
Arizona ships no HTTP server. The listener comes from a server adapter package:
[`arizona_roadrunner`](https://github.com/arizona-framework/arizona_roadrunner) (roadrunner:
HTTP/1.1, HTTP/2, HTTP/3) or [`arizona_cowboy`](https://github.com/arizona-framework/arizona_cowboy)
(Cowboy). An app that only renders offline (`arizona_static`) needs neither.
To track unreleased changes, swap the version for a git ref:
```erlang
{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:
```bash
npm install @arizona-framework/client
```
```js
import { connect } from '@arizona-framework/client';
connect('/ws');
```
`connect` spawns its WebSocket worker via a static `new Worker(new URL('./arizona-worker.min.js',
import.meta.url), { type: 'module' })`, so a downstream Vite/Rollup/rolldown build detects it and
emits (and content-hashes) the worker automatically, with no runtime path to configure. Apps that
never call `connect` tree-shake the worker out entirely. Loading `arizona.min.js` standalone via
`<script type="module">` still works: the worker resolves as its sibling next to the client.
## Quick start
A page with an embedded counter.
### 1. The counter component
`id` and initial `count` come from the parent via `?stateful` (step 2), so `mount/1` just passes
them through:
```erlang
%% 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.
### 2. The parent page
A route's **root handler** is a stateful handler. It receives initial bindings:
```erlang
%% 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})
]}
).
```
### 3. The layout
The HTML shell. Loads the client runtime that connects over WebSocket:
```erlang
%% 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');
"""
]}
]}
]}
]).
```
### 4. Configure the server
Add `arizona` and the adapter to your app's `applications` list in `.app.src`:
```erlang
{applications, [kernel, stdlib, arizona, arizona_roadrunner]}
```
Arizona lists no server itself, so an app that renders pages offline only (`arizona_static`)
never boots one. Add `ssh` too if you serve the SSH transport.
Ensure `rebar3 shell` loads the config and starts your app by adding
to `rebar.config`:
```erlang
{shell, [
{config, "config/sys.config"},
{apps, [yourapp]}
]}.
```
Replace `yourapp` with your project's app name. Then declare routes in `config/sys.config`:
```erlang
[{arizona, [
{server, #{
adapter => arizona_roadrunner_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"}}
]
}}
]}].
```
The `server` map's required `adapter` key names the `arizona_server` module of the adapter
package (`arizona_roadrunner_server`, `arizona_cowboy_server`); every other key is that adapter's
own options.
A config file is read by `file:consult/1`, so every route entry must be a literal **term**. That
becomes visible once a route needs `middlewares`: a `{Module, Function}` pair is a term, but a
call such as `arizona_middleware:extract([path_bindings])` is not, and the node refuses to boot
with `bad term` -- reported at line 1 whatever line the expression is on, since the whole file is
one term. Wrap a parameterised step in a named 2-arity function, or declare the routes in Erlang
instead; see _Declaring steps_ in `arizona_middleware`.
`rebar3 shell` reloads dependency modules on recompile. If that logs a `SUPERVISOR REPORT` for
the listener every time you save, add `{app_reload_blacklist, [roadrunner]}` to the `shell` block:
reloading a module purges its old code, which kills any process still lingering in it -- a
listener sits blocked in its own loop, so it is exactly that. Arizona's own reloader reads the
same option.
### 5. Run it
```bash
rebar3 shell
```
Open <http://localhost:4040> and click the buttons -- the server renders the initial HTML, then
pushes minimal diffs over WebSocket as the count changes.
## Documentation
See [docs/architecture.md](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](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.
## Sponsors
Arizona is open source and maintained on personal time. If you or your company find it useful,
consider [sponsoring](https://github.com/sponsors/williamthome).
I also accept coffees ☕
[](https://www.buymeacoffee.com/williamthome)
<a href="https://github.com/sponsors/williamthome">
<img
src="https://raw.githubusercontent.com/williamthome/williamthome/sponsorkit/sponsors.svg"
alt="Sponsors"
/>
</a>
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup,
testing guidelines, and contribution workflow.
### Contributors
<a href="https://github.com/arizona-framework/arizona/graphs/contributors">
<img
src="https://contrib.rocks/image?repo=arizona-framework/arizona&max=100&columns=10"
width="15%"
alt="Contributors"
/>
</a>
## Star History
<a href="https://star-history.com/#arizona-framework/arizona">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date&theme=dark"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date"
/>
<img
src="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date"
alt="Star History Chart"
width="100%"
/>
</picture>
</a>
## License
Copyright (c) 2023-2026 [William Fank Thomé](https://github.com/williamthome)
Arizona is open-source under the Apache 2.0 License on
[GitHub](https://github.com/arizona-framework/arizona).
See [LICENSE.md](LICENSE.md) for more information.