A web framework for building server-driven, real-time applications as MirageOS unikernels using Datastar for hypermedia exchange over SSE.
- Server-driven: All rendering happens on the server. No client-side JS framework.
- Real-time: Every view is a live SSE stream. State changes push instantly to all connected clients.
- Unikernel-ready: Same code runs on Unix (dev) and as a MirageOS unikernel (prod). Zero filesystem access at runtime.
- Minimal: Simple map-based router, no middleware chains to debug, no magic.
Every page registers two HTTP routes:
GET /path→ serves a lightweight HTML shell (the “shim”) that boots Datastar and opens the SSE connectionPOST /path→ opens a persistent SSE stream that pushes rendered HTML on every state change
let _view = View.define ~datastar_js hub "/" (fun _req ->
Lwt.return (Template.render_string tpl ~models:[("count", string_of_int !counter)]))Actions are POST handlers with content-addressed paths (SHA256 of the name). When an action returns No_content, the framework automatically broadcasts a refresh to all SSE clients.
let inc_path = Action.define hub "counter/inc" (fun _req ->
incr counter;
Lwt.return Action.no_content)Use the path directly in Datastar attributes:
<button data-on:click="@post('{{ inc_path }}')">+1</button>The hub manages connected clients with Lwt_condition fan-out. Hash deduplication skips sending when rendered HTML hasn’t changed.
Optional persistent state via Irmin. Connect a store to a hub and state changes automatically trigger SSE broadcasts:
let* store = Store.create () in
Store.connect_hub store hub;
(* Any Store.set call now triggers Sse.notify_all *)Static files are compiled into the binary at build time via Nushell codegen — required for unikernel targets with no filesystem. Assets get content-addressed URLs with immutable cache headers.
nix develop
dune exec examples/counter/main.exe
# Open http://localhost:8080| Example | Port | Description |
|---|---|---|
counter | 8080 | Minimal: single counter with SSE push |
| =game-of-life= | 8081 | Conway’s GoL with continuous game loop |
todo | 8082 | CRUD: Irmin-backed todo list |
chat | 8083 | Multi-user real-time chat room |
| Library | Role |
|---|---|
h1 | HTTP/1.1 protocol |
paf | MirageOS HTTP server (Protocol-Agnostic Fibers) |
datastar | Datastar SSE SDK (datastar-sdk-ocaml) |
jingoo | Jinja2-compatible templating |
irmin | Git-like database for persistent state |
lwt | Cooperative concurrency |
digestif | SHA256 for content addressing |
yojson | JSON serialization |
MIT