Wun
Clojure-first · SDUI for the post-LiveView era
One Hiccup tree. Three native renders.
Counter
42
Web · Replicant
Counter
42
iOS · SwiftUI
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}} "+"]]])});; An intent is a Malli-typed action with a *pure* morph.;; Same fn runs server-side (authoritative) and client-side;; (optimistic prediction). Reconcile via UUID.(defintent :counter/inc {:params [:map] :morph (fn [state _] (update state :n (fnil inc 0)))})
(defintent :counter/by {:params [:map [:n :int]] :morph (fn [state {:keys [n]}] (update state :n (fnil + 0) n))});; A component is a namespaced keyword + per-platform renderers.;; Schema is Malli; capability negotiation reads :since to decide;; native-vs-WebFrame fallback for older clients.(defcomponent :myapp/Card {:since 1 :schema [:map [:title {:optional true} :string]] :loading :inherit :fallback :web :ios "Card" :android "Card"})wun new app myapp.How it works →The full server-driven UI loop, end to end.What you get
defcomponent, defscreen, defintent are the entire authoring
surface. Framework primitives use the same APIs as your code —
:wun/Stack and :myapp/Card are indistinguishable to the runtime.
A Hiccup-shaped tree with namespaced keywords, sent as transit or JSON envelopes. Same shape lands on the web, in SwiftUI, and in Compose. No platform-specific schema to drift.
Optimistic by defaultThe same pure :morph runs server-side (authoritative) and
client-side (predictive). UI updates the moment the user clicks;
the round-trip just confirms.
Any component the native client lacks collapses to a sandboxed
:wun/WebFrame at the smallest containing subtree. New components
ship without breaking older clients.
The last tree stays rendered while the SSE stream is down. Clicks queue locally, replay on reconnect, and dedup against an LRU on the server so retries never double-apply.
One CLI for the whole monorepowun new app, wun add component, wun dev, wun status. The
scaffolders splice idempotently across cljc, Swift, and Kotlin so
you never edit five places by hand.
How a tap travels
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
-
Install the CLI.
Terminal window git clone https://github.com/Holy-Coders/wun.gitcd wun && ./install.shwun doctor -
Scaffold an app.
Terminal window wun new app myappcd myappnpm install -
Run all four surfaces in parallel.
Terminal window wun dev # server :8080 + shadow-cljs watch :8081wun run ios # macOS demo via swift run (separate term)wun run android # Compose Desktop via gradle runOpen
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.mdat 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 exposingwun 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.