Lua, in the browser, mounted per route.
Pages that need interactivity declare a data-lua-module attribute on the body. A
wasmoon-compiled Lua engine boots, fetches only that module, and runs it. No virtual DOM, no React — signals +
hyperscript in a ~120-line Lua library.
data-lua-module="..."
mounts library + page
renders into #app
Live demos
A single signal driving a text node and two button handlers. The whole demo is ~30 lines of Lua.
Run demo →-- pages/counter.lua local count, setCount = reactive.signal(0) local root = h("div", {}, h("p", {}, function() return "Count: "..count() end), h("button", { onClick = function() setCount(count()+1) end, }, "+") ) dom.mount(root, "#app")
A reactive.effect re-renders the list whenever the signal changes. Input wired via dom.on event bridging.
Run demo →-- pages/todolist.lua local items, setItems = reactive.signal({}) local draft, setDraft = reactive.signal("") reactive.effect(function() list.innerHTML = "" for _, v in ipairs(items()) do dom.append(list, h("li", {}, v)) end end)
Edit and run any module in the browser. Each run gets a fresh engine; a count hook stops runaway loops, and the two demos above run in it unmodified.
Open playground →-- pages/playground.lua (excerpt) function playground.run() local src = __src __src = nil local chunk, err = load(src, "@playground.lua", "t") if not chunk then return err end debug.sethook(trip, "", LIMIT) local ok, e = pcall(chunk) debug.sethook() if not ok then return tostring(e) end end
A ledger matched against a provider settlement report. Integer cents, one verdict per reference, verdicts re-run as you edit.
Run demo →-- pages/reconcile.lua (excerpt) elseif l.amount ~= s.amount then verdict = "Amount mismatch" reason = string.format("Settled %s than the ledger: %s vs %s %s", gap(s.amount - l.amount, l.currency), money(s.amount), money(l.amount), l.currency)
Pages without a module ship zero wasmoon.
boot.js returns early when the body carries no data-lua-module. The framework JS is
fetched once per origin and cached; every page module after that is a kilobyte or two.
Selected web builds
Real-time sprint board with AI assist
Live sprint board over Jira Cloud. SignalR streams delta updates so the board reacts the instant tickets move. A D3 force graph lays out tag-association clusters across the backlog. An OpenRouter-backed chat assistant answers sprint-state questions inline.
Fintech, banking, and multilingual WordPress at scale
Angular and .NET applications inside banking environments. Over sixty payment-API integrations spanning fintech rails. Multilingual WordPress at enterprise scale — localized content, custom plugins, integrations against legacy systems. Client names withheld; happy to walk through any of it under NDA.