Skip to content

Home

Fitz LiveViews

# Fitz LiveViews

Real-time server-rendered UI for Fitz.

WebSocket diffing Β· Zero JS build Β· Phoenix LiveView-inspired


Status: Phase 4 β€” LiveComponents + a real app πŸŽ‰

Everything you need to build a real-time UI works end-to-end, and the library now has a flagship application to prove it:

  • Phase 1 β€” Html type and templating with XSS-safe flv() escape
  • Phase 2 β€” LiveView core (@get + @ws + live_layout) β€” the counter
  • Phase 3a/3b β€” Forms via data-flv-submit, shared state, ws.broadcast(...), and the HTML parser + tree diff engine β†’ compact patches over the wire, DOM state preserved (input values, focus, cursor)
  • Phase 4 β€” LiveComponents (@live_component, per-instance state) plus the kanban and dashboard showcases, and the flagship Admin ABM β€” a full back-office over Postgres with a component catalog and a runnable component gallery

New to it? Start with the course. See the Roadmap for what's next.


What is this?

You write UIs like HTTP handlers. Fitz LiveViews takes care of updating them in the browser via WebSocket β€” without you writing a line of JavaScript.

The "hello world" is a real-time counter:

type CounterState { count: Int = 0 }

fn render(state: CounterState) -> Html {
  return html("""<div id="counter-app">
    <p>Count: {state.count}</p>
    <button data-flv-click="increment">+1</button>
  </div>""")
}

@get("/")
fn counter_page() -> Response {
  return html_response(
    live_layout("/live/counter", "counter-app", render(CounterState { count: 0 }))
  )
}

@ws("/live/counter")
async fn counter_socket(ws: WsConn<LiveFrame>) {
  let state = CounterState { count: 0 }
  let last_html = render(state).raw
  loop {
    let frame = ws.recv()?
    if (frame.event == "increment") {
      state = CounterState { count: state.count + 1 }
    }
    let new_html = render(state).raw
    let patches = diff_html(last_html, new_html)
    ws.send(LiveFrame { html: new_html, patches: patches })?
    last_html = new_html
  }
}

@server(3000)
fn main() => 0

Click the button, the count updates without a page reload. No React, no bundler, no useState, no fetch call. Just a Fitz function.


Why?

The web today gives you two options that both hurt:

  • Server-rendered classics (Django, Rails, PHP) β€” simple but every click reloads the page. Feels dated.
  • SPAs (React, Vue, Angular) β€” modern feel, but drowning in complexity: bundlers, two codebases, doubled types, separate deploys.

There is a third way that is underrated: Phoenix LiveView (Elixir), Hotwire (Ruby), HTMX. The server renders HTML, a tiny JS client morphs the DOM on updates. No bundle. No duplicated state. No SPA.

But none of those third-way tools exist for a compiled, natively-typed language. fitz-liveviews fills that gap.


What makes this different

Five things you don't get anywhere else in one package:

  1. Server-side state, no duplication β€” the database is your source of truth. Client is a thin projection.
  2. Typed end-to-end β€” state, events, templates, all validated by the Fitz compiler. No defineProps<T>(), no tsconfig.json.
  3. WebSocket-native β€” Fitz has @ws and WsConn<T> in the core language. No Socket.io, no Phoenix Channels layer.
  4. Zero build step β€” no Vite, no Webpack, no npm install, no package.json. fitz dev and go.
  5. Native binary output β€” fitz build gives you one file. No Node, no BEAM, no Python runtime. Deploy is docker run.

Why this works in Fitz

Most "LiveView-style" projects in other ecosystems have to ship an entire runtime alongside the framework: a WebSocket layer, an async story, a session/state model, sometimes even a mini language for events. That is why they are heavy and hard to type end-to-end.

Fitz LiveViews is intentionally thin because the language already brings the pieces that matter:

  • @ws("/path") + WsConn<T> β€” typed WebSockets are a first-class citizen of the language, not a library. The diff channel just uses them.
  • async fn + .await + Future<T> β€” the server never blocks on an event handler. No threadpool tuning, no callback hell.
  • @get/@post + native ORM β€” the same handler that renders the initial HTML can also query the database, with no ceremony.
  • Compiler-enforced types β€” state, events, and templates are all checked at build time. No defineProps<T>(), no runtime schema drift.
  • fitz run ↔ fitz build parity β€” hot reload in dev, single standalone binary in prod. The runtime story is the same in both.

The Elixir community solved this by pinning everything to OTP + Phoenix. JS/Ruby/Python solve it by inventing the WebSocket + serialization layer outside the language. Fitz is closer to the Elixir model: the library leans on primitives that the compiler already validates.


What it is for

Concrete use cases where LiveViews shines:

  • Collaborative editing β€” kanban boards, shared documents, live cursors (see the kanban example).
  • Real-time dashboards β€” metrics, order status, alerts that update without a page reload.
  • Live-validated forms β€” server checks each field as you type, errors appear in place with no fetch roundtrip written by hand.
  • Multi-user apps β€” chat, comments, live reactions (see the chat example).
  • Admin panels β€” internal tools where you want fast iteration and don't want to maintain a separate SPA + API.

Not a fit: fully offline-first apps, heavy client-side animation, or apps that must work without a persistent WebSocket connection.


The advantage: Vue+Vuetify ergonomics with a LiveView reactive model

As the library grows into @live_component + templates + (eventually) a UI companion, writing components will feel a lot like Vue with Vuetify: props, slots, ready-made buttons/inputs/dialogs, no CSS by hand.

But the reactive model stays LiveView, not Vue: state lives on the server, events travel over WebSocket, the server pushes diffs to the client. Vue does it the other way around β€” reactivity in the browser with a virtual DOM.

That combo is the sweet spot that neither side covers alone:

  • Elixir LiveView is powerful but the HTML/CSS story is bare.
  • Vue+Vuetify is beautiful but forces you to maintain an API + client state + sync layer.

Fitz LiveViews bets on having both at once β€” the ergonomics of Vue+Vuetify with the simplicity of a server-authoritative model, on top of a compiled binary and a typed core.


Comparison

Vue + FastAPI Phoenix LiveView HTMX + Go fitz-liveviews
Server language Python Elixir Go Fitz
Client bundle 30–100 KB ~5 KB ~10 KB ~5 KB
Compile step for client Vite / Webpack mix compile None None
Native binary ❌ (Python + Node) ❌ (BEAM VM) βœ… βœ…
Typed state ↔ template Volar + TS setup Runtime ❌ Compiler-enforced
WebSocket layer Manual (socket.io) Phoenix Channels ❌ (HTTP only) Native WsConn<T>
Server-side diff engine ❌ βœ… ❌ βœ…
Auto OpenAPI + AsyncAPI ❌ ❌ ❌ βœ… (from Fitz)
Deploy Node + Python + Nginx BEAM release Single binary Single binary

Quick start

Prerequisites: the fitz binary. The Fitz install guide covers Windows, macOS, Linux, plus VSCode extension setup and troubleshooting.

# Clone the repo
git clone https://github.com/Thegreekman76/fitz-liveviews
cd fitz-liveviews

# Run the counter (real-time, single user)
cd examples/counter
fitz run

Open http://127.0.0.1:3000/. Click +1 and the number updates in real time β€” no page reload, no fetch, no JavaScript build step.

For the multi-user chat, cd examples/chat && fitz run and open the URL in two browser windows to see broadcast in action.


Where to next

  • Learn Fitz LiveViews (course) πŸŽ“ β€” the guided path from zero to a deployable app, one live component at a time
  • HTML primitives β€” how Html, flv(), h_join, and friends work
  • LiveViews β€” the pattern, the client runtime, the diff engine, and known limits
  • LiveComponents 🧩 β€” reusable server-rendered components with per-instance state
  • UI components catalog 🧰 β€” DataGrid, forms (tabs/stepper), modal, toasts, tree, cascade selects… with a snippet each
  • Admin ABM example ⭐ β€” flagship back-office: the full runnable reference for every component
  • Component gallery 🧰 β€” every component on its own, no DB, in-memory β€” the copy-paste source
  • Counter example β€” the "hello world"
  • Chat example β€” multi-user real-time with broadcast + patches
  • Kanban example ⭐ β€” flagship showcase: shared state, data-flv-value-* payloads, and a compact 3-column responsive board with a per-card @live_component("card_editor")
  • Dashboard example 🧩 β€” grid of six independent metric_tile instances driven by a zero-branch parent WS handler

Credits

Fitz LiveViews stands on the shoulders of:

Bringing the third-way idea to compiled, natively-typed languages.


License

MIT. See LICENSE.