Alvaro Guzman Résumé ↓
← Work
Web · browser runtime

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.

1 · Server
stamps body
data-lua-module="..."
2 · boot.js
reads attr,
mounts library + page
3 · wasmoon
lua.doFile()
renders into #app

Live demos

Demo 1
Reactive counter

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")
Demo 2
Mutable list with effect

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)
Demo 3
Lua playground

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
Demo 4
Reconciliation checker

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)
Runtime topology

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.

wasmoonLua 5.4 → WASM
reactive.luasignal / effect / computed
h.luahyperscript dom builder
manifest.jsonlibrary mount list
pages/*.luafetched on demand
lua-framework.js~114 KiB
3× library .lua~2 KiB
page module .lua~1–2 KiB
non-lua routes0 bytes

Selected web builds

01

Real-time sprint board with AI assist

Shipped

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.

Angular.NETSignalRD3Jira CloudOpenRouter
02

Fintech, banking, and multilingual WordPress at scale

Ongoing · NDA

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.

Angular.NETWordPresspayment APIsi18nbanking