Building hypermedia apps with Datastar and Clojure
Signals, SSE, server-shaped routes, and a native shell that replaces a React Native app
Datastar is a hypermedia framework, and the whole pitch fits on an index card: one script tag,
a set of data-* attributes, and a server that answers requests with HTML and signal
patches over Server-Sent Events. That's it. That's the framework. This series teaches it from
Clojure, with no SDK. You write the wire format yourself in part 2 (it's a handful of small
functions, so don't get nervous), and once you have, nothing the browser does is a mystery.
Everything builds toward one small app, Brew Board: a coffee counter where customers configure drinks and place orders, and a barista moves them along a live board. Along the way you'll decide what belongs in a signal and what doesn't (spoiler: less than you'd think), shape the backend's routes around commands and one long-lived stream, and finally serve the very same routes to a thin native shell that takes the place of a React Native app. Yes, really. Parts 1–6 are the core. Parts 7 and 8 are optional: part 7 is for building a mobile app, and part 8 weighs the alternative architectures.
It's a toy, but a toy in the useful sense, the way a model railway is useful if you want to
understand switching. State is an atom, there is no auth and no database, and nothing is tuned.
Each part has a standalone lesson in clojure/ with a narrated demo, an exercise track
with failing tests, interactive figures to step through, and recall cards. And wherever a usage
decision depends on how Datastar works inside (how it reads the stream, how signals track
dependencies, how the morph matches elements), the post opens the hood and explains that piece
from the source.
How this series was made. It was written with Claude Code, Anthropic's coding agent, on top of Learning Kit, Dan Sutton's toolkit for teaching a topic by building a toy version of it. Learning Kit supplies the site shell, the predict-then-reveal figures and recall cards, the exercise track, and the CI that checks the code, the tests and every quoted excerpt. Thank you, Dan.
Practice
Reading about it isn't the same as building it. A few things help it stick:
- Predict, then reveal. Figures ask for your guess before they show the answer. Being wrong first is the point.
- Build it. Every lesson's key functions with the bodies removed and the tests left in.
- In the wild. The same mechanisms in the source of real implementations, verbatim and linked to the exact lines. Open each post's section after its exercises.
- Review. A few spaced-repetition cards a day from the parts you've read. Each post opens with a warm-up from the one before.
The code
Each part has a standalone lesson in clojure/src/lessonNN and go/lessonNN with a
runnable demo; the capstone puts everything together.
And here's where it all comes together. The capstone is the whole of Brew Board in clojure/src/brewboard: the hand-written
SSE writer, the menu and pure handle-command, whole-view renders for the customer
and the board, the broadcast hub, the reitit routes, and the native-shell routes. Every piece you built, in one place. Start it and
open the two pages side by side, in two tabs of one browser. That's safe because part 6 gave
every tab its own id, so the tabs don't step on each other's drafts:
cd clojure && clojure -M -m brewboard.main # PORT env var, default 8080
# customer: http://localhost:8080/ board: http://localhost:8080/board
Add a medium oat latte as Ada, place the order, advance it twice on the board, and mark oat milk
sold out. Then just watch: every open page follows along, and the customer's drop-down shows oat as sold out without
a reload. It's a small thing, and it's oddly thrilling the first time. clojure -M -m brewboard.demo runs the same session headless over real HTTP
and prints what each stream received, for those of us who like to see the receipts.
One more thing. native/ios and native/android hold small reference shells, a
WKWebView app and a WebView app, that load the same server, apply the served
/native/path-config rules, and implement the BrewShell bridge from
part 7. They aren't built in CI (Xcode in CI is its own adventure); native/README.md says what was checked and how to run
them against localhost.
Thanks to Fabian Merkle, who spoke about Datastar at Clojure/Conj 2026, for reviewing the series and for feedback that changed it. Part 8's indicator trade-off and its command endpoint, the error handling in part 6's broadcast, part 6's "Where this goes next" and the half-open connections in part 7 all came out of his notes. Any mistakes that survived are mine.