Skip to content

Wun

Server-driven UI for native apps and the web. One vocabulary, one wire, three platforms — rendering in lock-step.

Clojure-first · SDUI for the post-LiveView era

One Hiccup tree. Three native renders.

localhost:8081

Counter

42

Web · Replicant

9:41

Counter

42

iOS · SwiftUI

Wun

Counter

42

Android · Compose

The server holds state. Patches stream over a single SSE channel. Every client binds the same namespaced keywords to native widgets. The triptych above is the same [:wun/Stack ...] rendered through three different render registries — flat material on the web, SwiftUI shapes on iOS, Material 3 on Compose. No fake screenshots, no platform-specific schema.

The whole authoring surface fits in one screen

Three macros — defcomponent, defscreen, defintent. Framework primitives use the exact same APIs as your application code. There is no privileged path.

;; A screen is a path + a render fn over state.
;; Page metadata (:title, :theme-color) rides on every patch envelope
;; so :title becomes <title> on web, .navigationTitle on iOS, and
;; the Compose Window title on Android — automatically.
(defscreen :counter/main
{:path "/"
:present :push ; :push or :modal
:meta (fn [s] {:title (str "Counter " (:n s 0))})
:render (fn [s]
[:wun/Stack {:gap 12 :padding 24}
[:wun/Heading {:level 1} "Counter"]
[:wun/Text {:variant :h2} (str (:n s 0))]
[:wun/Stack {:direction :row :gap 8}
[:wun/Button {:on-press {:intent :counter/dec}} "−"]
[:wun/Button {:on-press {:intent :counter/inc}} "+"]]])})

What you get

How a tap travels

wun-serverClient (web · iOS · Android)Userwun-serverClient (web · iOS · Android)Userholds authoritative stateand the component vocabularydrop pending entry by intent idpredicted == authoritativeSSE bootstrap {tree, state, screen-stack}1tap :wun/Button2optimistic morph(predicts next state)3POST /intent {id, intent, params}4validate · run :morph · update state5SSE patch (replace · insert · remove)6

The full architecture write-up — head metadata, hot-cache hydration, reconnect semantics — lives under Architecture.

Where Wun fits

Capability Wun LiveView Hotwire Native React Native
State lives on the server Yes — single SSE channel, optimistic morphs. Yes — stateful socket per tab. Yes — Turbo streams, server templates. No — state is on the client.
Native widgets per platform SwiftUI · Compose · DOM (one tree, native renderers). DOM only. Native shell + WebView fragments. Bridges to native, JS layer in between.
Cross-platform vocabulary Namespaced keywords (:wun/Stack, :myapp/Card). HTML elements + LiveComponents. Turbo Frames + Stream actions. React components, JSX.
Forward-compat fallback Unsupported components collapse to :wun/WebFrame. — Native shell handles unknown URLs as web. App update required.
Optimistic UI Same :morph runs server + client; UI predicts. JS hooks for client-side prediction. Optimistic HTML via Turbo Frames. App-level (Redux, Zustand, etc.).

The bet: keep UI state on the server, ship structure to clients as data, let each platform render that structure natively. The result feels native because it is native — but no platform duplicates UI logic.

Quick start

  1. Install the CLI.

    Terminal window
    git clone https://github.com/Holy-Coders/wun.git
    cd wun && ./install.sh
    wun doctor
  2. Scaffold an app.

    Terminal window
    wun new app myapp
    cd myapp
    npm install
  3. Run all four surfaces in parallel.

    Terminal window
    wun dev # server :8080 + shadow-cljs watch :8081
    wun run ios # macOS demo via swift run (separate term)
    wun run android # Compose Desktop via gradle run

    Open http://localhost:8081, an iOS simulator, and a Compose window. Tap + in any of them — the other surfaces update on the next patch.

The full walkthrough lives at Your first app.

Built for AI agents too

Wun ships first-class agent surfaces. Drop into any new app and a coding agent — Claude Code, Cursor, Cline, Continue — orients in seconds:

  • CLAUDE.md / AGENTS.md at repo root — project orientation, the three macros, common gotchas.
  • skills/ — short, narrow how-to playbooks for canonical Wun tasks (wire an intent, add a screen with a form, ship a breaking change).
  • mcp/server.mjs — a Model Context Protocol server exposing wun status, wun add component, and the doc resources to any MCP-aware client.

See AI integration for the full surface.

Inspired by

Phoenix LiveView for the stateful-socket model and head merging; Hotwire Turbo and Hotwire Native for the WebFrame fallback posture; the broader server-driven UI movement for the “structure, not pixels” wire model. The Clojure side leans on Malli for schemas and the standard data-as-API + macro patterns to keep the framework surface tight.

Status

The framework has been through a production-readiness pass: hardened SSE transport (heartbeat, backpressure, rate limit, CSRF, telemetry), wire envelope v2 with key-aware list diffing, forms + streaming uploads, theme primitives that cascade server → client, in-process pubsub + presence, and an error boundary that keeps the SSE stream alive when a screen render throws. Web client rebuilt on Replicant — zero React, zero JS deps. iOS and Android decode v2, echo CSRF, and mirror the theme. See the changelog for what’s landed and the open issues for what’s next.