Why OTP for multi-surface apps
View SourceMost UI frameworks implement crash recovery with try/catch, state management with global stores, concurrency with goroutines or async/await, distribution with gRPC. Raxol gets all of that from OTP.
The natural mapping
| OTP concept | TUI equivalent | What you get |
|---|---|---|
| GenServer | Elm update loop | init/1 -> update/2 -> view/1, managed by the runtime |
| Process | Component | Each Component can run in its own process |
| Supervisor | Crash recovery | A Component crashes, it restarts. The rest of the UI doesn't notice |
| Hot code swap | Live reload | Change view/1, save, running app updates. No restart |
:ssh | SSH serving | Built into Erlang. No dep, no daemon, just :ssh.daemon |
libcluster | Node discovery | Gossip, DNS, Tailscale. Nodes find each other automatically |
send/2 | Inter-component messaging | No event bus library. Just processes sending messages |
| ETS | State management | Fast shared state without serialization overhead |
These aren't analogies. They're the actual implementations.
What this means in practice
Crash isolation
In Ratatui or Bubble Tea, if a component panics, your whole app dies. In Raxol:
process_component(UnstableWidget, %{path: "/dev/random"})The supervisor restarts the component and renders the next frame. OTP was built for this.
There is one place this stops. On Unix and macOS the terminal backend is a termbox2 NIF, and a NIF runs inside the VM's own address space. Segfault there and the whole node goes down, supervisor or not.
The blast radius is small, though. That NIF only draws: cell writes, cursor
moves, present. Input never touches it. Keystrokes arrive over OTP's own
prim_tty, and a canary in CI watches
that path in case OTP moves it. Windows skips the NIF entirely and falls back to
the pure Elixir IOTerminal, so nothing native is loaded there at all.
When the VM does die that way it dies without putting your terminal back. Run
reset; there is a note in the
quickstart.
Hot reload
Erlang's code server supports hot swapping at the module level. Save a file, and the running app picks up the new view/1 on the next render cycle. No reconnection, no state loss. Same mechanism that lets telecom switches upgrade without dropping calls.
SSH serving
Erlang ships with a full SSH server. Raxol wraps it:
Raxol.SSH.serve(MyApp, port: 2222)Each connection gets its own Lifecycle process with its own state. The whole thing is 4 modules, ~400 lines, because the hard part is in Erlang's :ssh.
Textual added SSH in 2024 via textual-serve, wrapping an external library. Bubble Tea and Ratatui have community wrappers.
Distribution
BEAM was designed for distributed systems. Raxol's swarm module builds on that:
Raxol.Swarm.Discovery.start_link(strategy: :tailscale, node_basename: "raxol")
Raxol.Swarm.TacticalOverlay.update_entity(:unit_1, %{position: {10.0, 20.0, 0.0}})Nodes are BEAM nodes. Messages are Erlang messages. CRDTs merge with pure functions.
Three rendering targets
A TEA module is init/1, update/2, view/1. The rendering target is a runtime decision:
- Terminal: Lifecycle renders to a screen buffer, diffs, writes ANSI
- Browser:
Raxol.LiveView.TEALivehosts the same module in Phoenix, bridges events - SSH:
Raxol.SSH.Sessionwraps Lifecycle per-connection
One app, three outputs.
AI agents
An agent is a TEA module where input comes from LLMs. Same init/update/view, same supervision. The framework is ~300 lines because most of it is OTP:
Agent.Sessionis a GenServer wrapping LifecycleAgent.Teamis a SupervisorAgent.CommisGenServer.call/castwith Registry lookupsAgent.Backend.HTTPisStream.resourceover SSE
Agents are processes. Teams are supervision trees.
The tradeoff
Raxol is slower per-operation than Rust (Ratatui) or Go (Bubble Tea). Buffer creation is 25us vs 0.5us. But a full frame still completes in 2.1ms, leaving 87% of the 60fps budget for your code. Numbers and methodology are in Benchmarks.
You give up raw microbenchmark speed. You get process isolation, hot reload, distribution, SSH, and multi-target rendering. For anything that has to keep running while you change it, that's a good trade.
Further reading
- Architecture: how the render pipeline works
- Agent Framework: AI agents as TEA apps
- Distributed Swarm: CRDTs and node discovery
- SSH Deployment: serving apps over SSH