The ancient Greeks had a word for truth: aletheia - literally "un-concealment." Truth wasn't something you asserted; it was something you revealed by stripping away what hid it.
This is the premise behind Aleth, a new server-driven UI library for Clojure. The core bet: no client-side computation. The server owns all truth. The client is a pure projection - it shows what it's told, nothing more.
In an age of increasingly complex frontend frameworks, this sounds almost naive. But there's a specific context where it makes profound sense: systems where humans supervise and AI writes the code.
Modern web UIs are distributed systems hiding inside your browser. State lives in Redux stores, component local state, URL parameters, localStorage, and server databases. When something goes wrong, you triangulate across all of them.
Aleth eliminates this by making a radical constraint: the client cannot compute.
Where Datastar allows $count + 1 in attributes, Aleth has no expressions. Where React components derive state locally, Aleth demands the server send exactly what to display. The client becomes a terminal - it receives instructions and renders them.
;; Server computes everything
(defn increment-handler [req]
(sse-response
(fn [sse]
(let [{:keys [count]} (signals req)
new-count (inc count)]
(signals! sse {:count new-count})
(patch! sse "#counter" [:div {:id "counter"} [:span (str new-count)]])
(close! sse)))))
The client receives two things: a signal update ({:count 6}) and a DOM patch. It applies both. No logic, no decisions, no opportunities to diverge from truth.
Aleth uses Transit+JSON over Server-Sent Events. The wire protocol has three operations:
The server sends hiccup, the client morphs it into the DOM using Idiomorph. Signal changes trigger reactive bindings. That's the entire runtime.
;; Server renders initial page
(defn counter-page [count]
[:html
[:body
[:div (a/signals {:count count})
[:span (a/text :count) (str count)]
[:button (a/action "/increment") "+"]
[:button (a/action "/decrement") "-"]]]])
The a/signals, a/text, and a/action helpers emit data-* attributes. When Aleth's JavaScript loads, it discovers these attributes and wires them up. Click the button, POST to /increment, receive SSE response, update DOM. The HTML works before JavaScript loads; Aleth progressively enhances it.
Determinism. state -> UI is a pure function. Same signals, same render. No "it works if you refresh," no race conditions between client and server state.
Testability. You can property-test your entire UI:
(defspec render-is-deterministic 100
(prop/for-all [state (mg/generator signals-schema)]
(= (render state) (render state))))
Visual regression testing becomes trivial - render HTML, snapshot, compare. No client timing issues, no flaky tests.
Observability. Everything is inspectable. Aleth includes devtools (SSE inspector, signal viewer, schema panel) that show every state transition. Debug by reading the event stream, not by reproducing timing-dependent bugs.
Schema validation. Malli validates signals on both ends. Invalid states are rejected, not silently accepted.
Clean API. The library exports a single entry point with sensible helpers. Hot reload works correctly (using WeakSet to prevent duplicate bindings). The devtools use Shadow DOM for style isolation.
This is an early-stage library with some serious issues.
No tests. For a library whose core value proposition is correctness and determinism, this is ironic. The spec includes property-based test examples, but the implementation has none.
Memory leaks. Signal watchers are never cleaned up. In a long-running session, this will accumulate. Multiple locations in the codebase add watchers without corresponding removal logic.
The execute escape hatch. The wire protocol includes an execute operation that runs arbitrary JavaScript via js/eval. This is a security risk in any production system, even if intended only for debugging. It's the kind of backdoor that gets forgotten about.
No recovery after SSE failure. The connection has retry logic with exponential backoff, but there's no recovery mechanism once retries exhaust. The client just stops.
Stale DOM references. After morphing, references to old DOM nodes aren't invalidated. This can cause silent failures in bindings.
URL injection. The redirect handler doesn't sanitize URLs, creating a potential vector for malicious redirects.
Aleth is right for:
Aleth is wrong for:
The spec is honest about this: "For offline-first, consider Fulcro."
Every click goes to the server and back. On a local network, this is imperceptible. Over a 200ms round-trip, it's noticeable. The library's answer is "optimize the server," not "add client computation."
This is philosophically consistent but practically limiting. There are interactions where even 50ms of latency feels broken - typing in a search box, dragging to reorder a list, hovering to preview. Aleth doesn't try to solve these cases.
The comparison to Phoenix LiveView is instructive. LiveView makes the same server-centric bet but in Elixir's ecosystem where lightweight processes and low-latency WebSockets are first-class. Aleth is swimming upstream against browser realities.
Aleth represents an interesting point in the design space: what if we maximally simplified the client at the cost of server round-trips? For the right use cases - internal tools, AI-assisted development, admin interfaces - this trade-off makes sense.
The ideas are sound. The implementation needs work.
If you're building something in the sweet spot (low-latency server, forms-heavy workflow, prioritizing correctness over responsiveness), Aleth is worth watching. If you need production-ready today, wait for the tests, the memory leak fixes, and the security audit.
The name promises truth as unconcealment. The library isn't there yet - but the architecture points in an interesting direction.
Published: 2025-12-31
Tagged: ui clojure server-driven clojurescript