Arizona - a web framework for Erlang/OTP

Hello Erlang community!

I’m excited to share Arizona, a new web framework for Erlang designed to simplify building modern web applications.

Arizona leverages stateful views, WebSocket communication, and efficient DOM patching (using morphdom) to deliver a seamless developer experience.

It is completely based on the Phoenix LiveView framework for Elixir.

Key Features

  • Stateful Views: Views maintain their state in memory, making it easy to manage dynamic content.
  • WebSocket Integration: Real-time updates with minimal payloads sent over WebSocket.
  • Efficient DOM Updates: Only the changed parts of the DOM are updated, thanks to morphdom.
  • Simple Routing: Built on top of Cowboy, with straightforward route definitions.
  • Optional Layouts: Wrap views in reusable layouts for consistent UI structure.

Template Syntax

Arizona utilizes a templating approach where Erlang code is embedded within HTML
using curly braces {}. This allows dynamic content generation by executing Erlang
functions directly within the HTML structure. For example:

<ul>
  {arizona:render_list(fun(Item) ->
      arizona:render_nested_template(~"""
      <li>{Item}</li>
      """)
    end, arizona:get_binding(list, View))}
</ul>

No macros, no special syntaxes, just dynamic Erlang code embedded in static HTML.

Getting Started

  1. Create a new rebar3 app:

    $ rebar3 new app my_app
    
  2. Add Arizona as a dependency:

    {deps, [{arizona, {git, "https://github.com/arizona-framework/arizona", {branch, "main"}}}]}.
    
  3. Define routes, views, and layouts in sys.config and your application files.

  4. Start the server and connect via WebSocket to enable real-time updates.

Example

A simple counter example is included in the example repository.

It is incomplete but the code demonstrates how to create stateful views, handle events, and update the DOM efficiently.

A step-by-step of it is in the README of the Arizona repository (including a GIF).

Static Site Generation

The arizona_static module is designed for static site generation. It takes Cowboy routes, copies static files, and renders Arizona templates into .html files. It’s functional and handles simple tasks effectively, but it’s still in its early stages.

Important Notes

Arizona is currently in its early stages of development and does not have an official release yet. It is an initial concept and proof of concept (POC) that demonstrates the core ideas of the framework. While it is functional and can be experimented with, it is not yet ready for production use and has not been tested in any real-world production environments.

Arizona began as a project during the SpawnFest 2023, where it won first place in the usefulness category. After the competition, I put the project on hold due to personal commitments but resumed development for one month in June 2024.

Two weeks ago, I came across this post on the Erlang forums about redesigning the Erlang website with Next.js, which inspired me to redesign Arizona from scratch with a new and improved concept
.
This effort has also benefited from the collaboration of @paulo-f-oliveira, who has been instrumental in shaping the project.

I believe Arizona could potentially be used to build a new, dynamic, and community-driven website for Erlang, offering an alternative to traditional approaches.

While it is still a POC, I hope it can spark interest and collaboration within the community.
If you’re curious about the framework or would like to contribute to its development, feel free to reach out or check out the GitHub repository.

Together, we can explore whether Arizona could be a viable option for building the next-generation Erlang community site!

Documentation Status

The documentation for Arizona is currently incomplete and not yet polished. While the core concepts and functionality are described here, many areas are still undocumented or lack sufficient detail. This is a known issue, and improving the documentation is a high priority as the project evolves.

During the development of Arizona, the documentation will be progressively enhanced to include:

  • Comprehensive guides for getting started.
  • Detailed explanations of core concepts and architecture.
  • Examples and tutorials for common use cases.
  • API references with clear descriptions of all functions, types, and callbacks.

If you encounter any gaps or have suggestions for improvement, please feel free to open an issue or contribute directly to the documentation.

Your feedback is invaluable in making Arizona more accessible and developer-friendly!

Support

Starring the project on GitHub is a great way to show your interest and motivate me to keep pushing forward :sparkles:

Additionally, if you find Arizona useful, consider sponsoring me or buying me a coffee.

Your support helps me stay focused, dedicate more energy to the project, and keep development on track.

Every bit of support, big or small, makes a huge difference! o/

I’d love to hear your thoughts, feedback, and contributions!
Let’s build something amazing together. :rocket:

Links

22 Likes

Woah, fantastic!

As a Liveview user, that’s appreciated.

Ok, sorry for the slim answer, I am on the go and could not resist.

I have a silly toy project that I ported from Liveview to Erlang+unpoly.
That’s a perfect candidate to be rewritten in Arizona, and then I can give you much more detailed feedback (maybe through GH issues).

One immediate reaction is:

  • please consider renaming assign to bind

Again, congratulations for the effort!

Cheers,
Carlo

2 Likes

Perfect! Please consider doing this :smiley:

I swear I’ve thought about this change several times. I’ll definitely consider doing it!

EDIT: Done in Rename assign to bind to align with Erlang conventions by williamthome · Pull Request #294 · arizona-framework/arizona · GitHub

Thank you!

2 Likes

@williamthome is erlang 27 a strict requirement?

Yes, for now. It relies on the new json module (introduced in OTP-27), and in my opinion, using triple-quoted strings—also added in OTP-27—is the recommended approach for writing templates.

I’ll tackle template diffs next week—I’m aware of the current issues and want to address them properly. In the meantime, I’m working on a simple pubsub feature. As for “Arizona,” progress is slow but ongoing—we’re moving forward!

1 Like

Hi all!

I’m excited to announce a major API redesign for Arizona Framework - a complete rewrite focused on developer experience and performance.

What’s New

The new API delivers significant improvements:

  • Easier template syntax with compile-time optimization via parse transforms
  • Enhanced performance through zero-runtime template parsing
  • Better maintainability with cleaner, more testable codebase
  • Comprehensive testing with extensive E2E test coverage

The framework is still in early development stages but now has much more solid foundations and is actively being developed with community feedback in mind.

Try It Out

Clone the repo and explore the working examples as described in the README. While the examples are functional showcases rather than polished demos, they demonstrate real-world usage patterns:

  • Counter App
    • Layout + View with event handling and PubSub integration
  • Todo App
    • Complex state management and CRUD operations
  • Modal System
    • Component composition, slots, and dynamic overlays
  • Data Grid
    • Advanced data presentation and sorting functionality
  • Real-time App
    • Live data updates and WebSocket communication
  • Static Blog
    • Static site generation with layouts and dynamic routing

Technical Improvements

Core Engine

  • Compile-time template optimization - Parse transforms eliminate runtime template parsing overhead
  • Simplified template syntax - Clean {Erlang_Expression} syntax within HTML
  • Enhanced differential rendering - Optimized DOM patching with template fingerprinting
  • Improved variable tracking - More efficient dependency tracking for selective updates

New Modules

  • Static Site Generator (SSG) - Generate static HTML from dynamic components
  • Enhanced PubSub system - Built on Erlang’s pg module for real-time broadcasting
  • Streamlined routing - Cleaner route configuration with multiple route types

Developer Experience

  • Comprehensive testing - Growing test coverage with CI/CD quality gates
  • Hot code reloading - File watching with automatic browser refresh (requires manual configuration)
  • Better error handling - Improved debugging with stack traces and context

Development Tooling

Architecture Highlights

Arizona demonstrates a process-per-connection architecture where each WebSocket connection runs in its own arizona_live GenServer, providing natural fault tolerance and leveraging Erlang’s lightweight processes.

The framework showcases how compile-time optimization can be applied to web development - templates are compiled at build time rather than parsed at runtime, resulting in zero template parsing overhead.

Current Status & Next Steps

Important Note: Arizona is still in active development and not ready for production use. Current focus areas include:

  • API stabilization and documentation improvements
  • Security features implementation (CORS, CSRF, sessions)
  • Performance benchmarking and optimization
  • Community feedback integration

Feedback Welcome

This represents a significant evolution of the framework’s direction. I’d love to hear thoughts from the community on:

  • The compile-time optimization approach using parse transforms
  • Developer experience improvements and pain points
  • Real-world usage patterns you’d like to see supported
  • What features would be most valuable for your use cases

Try the examples and let me know what you think!

Your feedback is crucial for shaping Arizona’s development o/

5 Likes

Many web frameworks like yours use templates, as in the example in your original post:

<ul>
  {arizona:render_list(fun(Item) ->
      arizona:render_nested_template(~"""
      <li>{Item}</li>
      """)
    end, arizona:get_binding(list, View))}
</ul>

I’ve personally always preferred the Yaws approach where all the web content is represented as Erlang data, i.e. <Tag> Content </Tag> becomes {Tag, Content}.
This turns the mix of Html and Erlang into plain Erlang, making it easier to work with, as the “Html” can now be easily be composed using regular functions.
This also lets us use regular Erlang indentation & syntax color support in editors.
Additionally we can use regular Erlang tooling to check and test the html generation.

With this approach, your code would become something like this:

...
list(arizona:get_binding(list, View)) % render a list
...

%% helper functions

list(Elements) ->
   {ul, elements(Elements)}.

elements(Elements) ->
   lists:map(fun(E) -> {li, E} end, Elements).

Note: I made elements() it’s own function to show off that the “Html” can be composed from multiple functions.

Is there a obvious benefit to using the template aproach?

It seems to me that a naive template design would need to process and then edit the full template while the Yaws approach would have to do a “to io_list” conversion on all of the “Html” data to generate actual Html. This seems like a roughly comparable string processing workload.

Note: with both approaches one could probably cache large static chunks of Html string data, to speed up processing.

Well, maybe I don’t have a proper response to this question, but I think it is more a matter of personal preference.

I personally visualize a template more effectively by writing HTML instead of “elements,” which is why I made the design decision.

This is how the (updated) template looks using tree-sitter-arizona in Neovim:

It looks “natural” for me, but maybe it’s because I’ve been using this syntax for some time.

Explaining templates a bit more.. below is the compiled version of the template above generated via arizona_parse_transform module:

We split the template into static and dynamic parts, where static refers to HTML and dynamic refers to Erlang code. Dynamic parts are always “lazy” rendered, wrapped by a function. This way, we have an optimized diff applied to the DOM when something changes, because we only send the dynamic data that changed. In the example, we only update the dynamic list rendering if the list binding changes.

Hey everyone!

I’ve just released a Rebar3 plugin for the Arizona Framework that makes creating new projects extremely easy. Pretty basic, but it gets the job done.

What it does:

  • Interactive terminal menu with arrow key navigation
  • Creates Arizona hello world and presence apps from templates
  • Works globally once installed

Quick install:

% Add to ~/.config/rebar3/rebar.config
{plugins, [
    {rebar3_arizona, {git, "https://github.com/arizona-framework/rebar3_arizona.git", {branch, "main"}}}
]}.

Then just run rebar3 arizona anywhere and pick what you want to create!

Example:

$ rebar3 arizona
    _          _
   / \   _ __ (_)_______  _ __   __ _
  / _ \ | '__|| |_  / _ \| '_ \ / _` |
 / ___ \| |   | |/ / (_) | | | | (_| |
/_/   \_\_|   |_/___\___/|_| |_|\__,_|

https://github.com/arizona-framework/arizona

Use ↑↓ arrows or j/k to navigate, Enter to select, Esc/q to cancel

[●] Create new app
[ ] Cancel

## Step 2: Enter app name

Enter app name (or Esc/q to cancel): myapp

## Step 3: Choose template

Use ↑↓ arrows or j/k to navigate, Enter to select, Esc/q to cancel

[●] Hello world template
[ ] Presence template
[ ] Cancel

The interactive flow:

  1. Choose “Create new app” from the main menu
  2. Enter your app name (e.g., “myapp”)
  3. Select a template: Hello world (basic) or Presence (real-time example)

Requirements: OTP 28+ (needed for the interactive shell features)

2 Likes

Hey everyone! :waving_hand:

The Arizona Framework website is now live!

The cool thing? The website itself is built using Arizona! :cactus:
You can see the framework’s component system and static site generation in action.
Pretty neat to have a framework demo itself.

Check it out!

3 Likes

Arizona Just Got Some Nice Updates :cactus:

Arizona shipped a bunch of features:

Custom Reloaders - Build your own file watchers. CSS changes? Only reload stylesheets, keep your form state.

Action Lists - Return multiple server responses at once. Reply + redirect + reload in one go. No more hacks.

Middlewares - Auth, CORS, whatever. Works on all route types. Simple continue/halt pattern.

Plugins - Transform your entire config before startup. Auto-add auth to admin routes, inject environment stuff.

Better JS Client - Configurable logging (silent → debug), now on NPM as @arizona-framework/client.

Error Templates - Decent HTML error pages instead of raw Erlang dumps in the browser.

rebar3 Plugin - Interactive project generator. rebar3 arizona creates new apps with templates including a modern frontend setup (Tailwind CSS, ESBuild, hot-reloading).

Give It a Try

  1. Add rebar3_arizona to your global rebar3 config at ~/.config/rebar3/rebar.config:

    {plugins, [
        {rebar3_arizona, {git, "https://github.com/arizona-framework/rebar3_arizona.git", {branch, "main"}}}
    ]}.
    
  2. Create and run:

    $ rebar3 new arizona.frontend myapp
    $ cd myapp && npm install && rebar3 shell
    
  3. Visit http://localhost:1912:

  4. Start coding!

Edit any .erl file → automatic recompilation + page reload
Edit any .css file → smart CSS-only updates (keeps form state)

The frontend template gives you Tailwind CSS + ESBuild out of the box with proper hot-reloading integration.

Why It Matters

These solve real pain points from building actual apps.

Still WIP but getting solid.

The reloader + middleware combo is pretty sweet - you can integrate any build setup and handle requests however you want.

Worth a look if you’re doing web stuff in Erlang/OTP.

3 Likes

I wanted to give Arizona a try, now that Debian Stable has erlang 27.

But it seems it needs 28 now?

Hey @carloratm! Great to hear you’re interested in trying Arizona!

You’re absolutely right about the OTP version requirements. Here’s the good news:

Minimum OTP: 27 (so you’re all set! I need to add OTP 27 in the CI matrix.)

OTP 28 is only needed for the interactive template creation feature using rebar3_arizona. But you can totally create new projects without interactive shell using:

$ rebar3 new arizona.<template_name>

Available templates:

  • arizona.hello_world - Basic Arizona application
  • arizona.presence - Real-time presence tracking
  • arizona.frontend - Frontend-focused setup

So with Erlang 27, you can dive right in with something like:

$ rebar3 new arizona.frontend my_app

If you use Neovim, check out arizona.nvim for syntax highlighting support!

Check out all Arizona repositories.

Would love to hear how it goes! Feel free to reach out if you run into any issues or have questions o/

> rebar3 new arizona.hello_world eoq
===> OTP release 28 or later is required by rebar3_arizona. Version in use: 27.3.4.1
===> Errors loading plugin {rebar3_arizona,
                                   {git,
                                    "https://github.com/arizona-framework/rebar3_arizona.git",
                                    {branch,"main"}}}. Run rebar3 with DEBUG=1 set to see errors.
===> Template 'arizona.hello_world' not found.

Oh, sorry, my mistake. I had the minimum OTP version set to 28 in rebar.config, and I just fixed it. Update the plugin. It should work now.

Thank you.

This is rebar3 shell in the created Arizona app.
I used the arizona.hello_world template:

> rebar3 shell
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling eqwalizer_support
===> Compiling fs
===> Compiling ranch
===> Compiling cowlib
===> Compiling cowboy
===> Compiling arizona
===> Compiling _build/default/lib/arizona/src/arizona_token.erl failed
    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 56 │  -nominal category() :: static | dynamic | comment.
    │   ╰── bad attribute

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 57 │  -nominal line() :: erl_anno:line().
    │   ╰── bad attribute

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 58 │  -nominal content() :: binary().
    │   ╰── bad attribute


    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 41 │  -export_type([category/0]).
    │   ╰── type category() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 42 │  -export_type([line/0]).
    │   ╰── type line() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 43 │  -export_type([content/0]).
    │   ╰── type content() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 50 │      category :: category(),
    │                  ╰── type category() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 51 │      line :: line(),
    │              ╰── type line() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 52 │      content :: content()
    │                 ╰── type content() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 73 │      Category :: category(),
    │                  ╰── type category() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 74 │      Line :: line(),
    │              ╰── type line() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 75 │      Content :: content(),
    │                 ╰── type content() undefined

    ┌─ _build/default/lib/arizona/src/arizona_token.erl:
    │
 94 │      Category :: category().
    │                  ╰── type category() undefined

     ┌─ _build/default/lib/arizona/src/arizona_token.erl:
     │
 103 │      Line :: line().
     │              ╰── type line() undefined

     ┌─ _build/default/lib/arizona/src/arizona_token.erl:
     │
 112 │      Content :: content().
     │                 ╰── type content() undefined

     ┌─ _build/default/lib/arizona/src/arizona_token.erl:
     │
 120 │      Content :: content(),
     │                 ╰── type content() undefined

I see the issue now - those compilation errors are from Arizona using the new nominal types feature introduced in OTP 28.

Unfortunately, the drop of OTP 27 support occurred in the latest Arizona API refactor where I adopted nominal types for better type safety. This means both the rebar3_arizona plugin and the Arizona framework itself now require OTP 28+.

I understand this is frustrating, especially if you’re waiting for OTP 28 to be available in Debian repositories. The nominal types feature provides significant benefits for Arizona’s type system, but it does create this compatibility barrier.

You’ll need to upgrade to OTP 28 to use Arizona. Sorry for the confusion in my earlier responses - thanks for helping clarify the actual requirements!

Hi everyone! :waving_hand:

I’m excited to share the latest Arizona updates.

TL;DR

  • Erlang Term Templates: Write templates as pure Erlang terms with full editor support and compile-time validation
  • Markdown Support: Full GitHub Flavored Markdown with Arizona template syntax integration
  • Component Lifecycle: Proper cleanup with unmount/1 callbacks for timers, PubSub, and resources
  • File-Based Templates: Load templates from files with compile-time optimization
  • Template Flexibility: Mix Erlang terms, HTML, Markdown, and file-based templates freely
  • Many bug fixes: Enhanced stability, performance, and error messages

What’s New

1. Erlang Term-Based Template Syntax

I’ve added a new way to write templates using pure Erlang terms, giving you full editor support, syntax highlighting, and compile-time validation:

arizona_template:from_erl([
    {'div', [{class, ~"container"}], [
        {h1, [], [arizona_template:get_binding(title, Bindings)]}
    ]}
])

2. GitHub Flavored Markdown Templates

Content-driven applications can now use Markdown with full Arizona template syntax and full GFM support (tables, autolinks, strikethrough, task lists):

arizona_template:from_markdown(~"""
# Arizona Framework

Welcome **{arizona_template:get_binding(user, Bindings)}**!

{% This is a template comment }

{arizona_template:render_stateful(comment_section, #{
    id => ~"comments",
    post_id => arizona_template:get_binding(id, Bindings)
})}

- Item 1
- Item 2
""")

3. Stateful Component Lifecycle Management

Components now support proper cleanup with unmount/1 callbacks:

-module(timer_component).
-behaviour(arizona_stateful).
-export([mount/1, render/1, unmount/1]).

mount(Bindings) ->
    {ok, TimerRef} = timer:send_interval(1000, tick),
    arizona_live:is_connected(self()) andalso arizona_pubsub:join(~"updates", self()),
    arizona_stateful:new(?MODULE, Bindings#{timer_ref => TimerRef}).

% Automatic cleanup when component is removed
unmount(State) ->
    TimerRef = arizona_stateful:get_binding(timer_ref, State),
    timer:cancel(TimerRef),
    arizona_pubsub:leave(~"updates", self()),
    ok.

4. File-Based Template Compilation

Templates can now be loaded from files with compile-time optimization:

% HTML from files
arizona_template:from_html({file, "templates/user.html"})
arizona_template:from_html({priv_file, myapp, "templates/user.html"})

% Markdown from files
arizona_template:from_markdown({file, "content/blog-post.md"})
arizona_template:from_markdown({priv_file, myapp, "content/post.md"})

Parse transforms optimize file-based templates at compile time just like inline templates.

Bug Fixes & Improvements

This release includes numerous bug fixes and improvements.


I’d love to hear your thoughts! Have you tried Arizona? What would you like to see next? Let me know in the replies.

Happy coding! :cactus:

Notes:

  • Arizona requires Erlang/OTP 28+
  • v0.1.0 coming soon on Hex
  • These changes introduced breaking changes that are not reflected in rebar3_arizona and other Arizona Framework repositories (I’ll be updating them)

Hi @williamthome

I followed the quick start guide here: GitHub - arizona-framework/arizona: A web framework for Erlang/OTP

And the result is a page without styles, and when the button is clicked I get this error in the js console:

Uncaught ReferenceError: arizona is not defined
    onclick http://localhost:1912/:1

I suppose I have to run some command to compile the frontend assets?

Hi @carloratm!

Thanks for taking the time to try out Arizona!

Yeah, docs are really messy. The documentation is something I’ll be improving before publishing Arizona on Hex (probably in 2 or 3 weeks).

I believe you missed importing the JS script and initializing Arizona. To make the button clicks work you should expose Arizona globally. Arizona doesn’t inject anything into the HTML files.

See example:

% config/sys.config
[
    {arizona, [
        {server, #{
            enabled => true,
            routes => [
                {asset, ~"/assets/arizona", {priv_dir, arizona, ~"static/assets"}, []},
                {websocket, ~"/live", #{}, []},
                {view, ~"/", hello_world_view, #{title => ~"Arizona Framework"}, []}
            ]
        }}
    ]}
].
% src/hello_world_view.erl
-behaviour(arizona_view).
-compile({parse_transform, arizona_parse_transform}).
-export([mount/2]).
-export([layout/1]).
-export([render/1]).
-export([handle_event/3]).

mount(#{title := Title}, _Req) ->
    Bindings = #{id => ~"view"},
    Layout =
        {?MODULE, layout, main_content, #{
            title => Title
        }},
    arizona_view:new(?MODULE, Bindings, Layout).

% Layout is rendered only once when the page loads - it's never updated.
% Only the view content (inserted via render_slot) receives real-time updates.
layout(Bindings) ->
    arizona_template:from_erl([
        ~"<!DOCTYPE html>",
        {html, [], [
            {head, [], [
                {title, [], ~"Arizona Hello World"},
                {script, [{type, ~"module"}], ~"""
                import Arizona from '/assets/arizona/js/arizona.min.js';
                globalThis.arizona = new Arizona();
                arizona.connect('/live');
                """}
            ]},
            {body, [], arizona_template:render_slot(maps:get(main_content, Bindings))}
        ]}
    ]).

render(Bindings) ->
    arizona_template:from_erl(
        {'div', [{id, arizona_template:get_binding(id, Bindings)}], [
            case arizona_template:find_binding(name, Bindings) of
                {ok, Name} ->
                    [~"Hello, ", Name, ~"!"];
                error ->
                    arizona_template:from_erl({button,
                        [{onclick, ~"arizona.pushEvent('hello_world')"}],
                        ~"Say Hello!"
                    })
            end
        ]}
    ).

handle_event(~"hello_world", _Params, View) ->
    State = arizona_view:get_state(View),
    UpdatedState = arizona_stateful:put_binding(name, ~"World", State),
    {[], arizona_view:update_state(UpdatedState, View)}.

Key points:

  • The Arizona asset route from priv/static/assets
  • Import the JS module and expose Arizona globally: globalThis.arizona = new Arizona()
  • Connect to the WebSocket: arizona.connect('/live')
  • Use onclick="arizona.pushEvent('event_name')" to trigger events

Let me know if you run into any other issues!