Arizona

Erlang CINode.js CIHex.pmHex Docsnpm versionLicense

arizona logo

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 a thin DOM patcher.

🚧 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

Requirements

Installation

Add Arizona to your rebar.config dependencies. Cowboy is required for the built-in HTTP/WebSocket transport; skip it only if you write your own arizona_req adapter.

{deps, [
    {arizona, "~> 0.1"},
    cowboy
]}.

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/client
import { connect } from '@arizona-framework/client';

connect('/ws');

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:

%% 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 view is the route's root handler. It receives initial bindings plus the request:

%% src/my_page.erl
-module(my_page).
-include_lib("arizona/include/arizona_view.hrl").
-export([mount/2, render/1]).

mount(Bindings, _Req) ->
    {Bindings, #{}}.

render(Bindings) ->
    ?html(
        {main, [{id, ?get(id)}], [
            {h1, [], [~"Counter demo"]},
            %% id is required -- it&#39;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:

%% 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 &#39;/assets/arizona.min.js&#39;;
                    connect(&#39;/ws&#39;);
                    """
                ]}
            ]}
        ]}
    ]).

4. Configure the server

Add arizona to your app's applications list in .app.src:

{applications, [kernel, stdlib, cowboy, arizona]}

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"}}
        ]
    }}
]}].

5. Run it

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 for the full architecture reference -- module breakdown, op codes, dev-mode file watchers, custom schemes/proto_opts, and imperative startup.

Sponsors

If you like Arizona, please consider sponsoring me. I'm thankful for your never-ending support ❤️

I also accept coffees ☕

"Buy Me A Coffee"

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for development setup, testing guidelines, and contribution workflow.

Contributors

<img src="https://contrib.rocks/image?repo=arizona-framework/arizona&max=100&columns=10" width="15%" alt="Contributors" />

Star History

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

License

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.