stack-RFC-022 — Kroma Web Backend: the declarative UI on a browser canvas (WebGPU)
1. Problem
The Koder Stack has app surfaces (dashboards, forms, settings) that today are HTML, and — per [[stack-RFC-019-koder-ui-render-strategy]] — are migrating to Kroma on the desktop (terminal, window, .deb; see done/070`done071).
Open question (owner, 2026-06-25): can the *same* declarative Kroma App` run in a web browser, rendering to a canvas instead of HTML — so we reuse one UI codebase across desktop and web?
2. Decision
Yes — add a Kroma Web Backend: a 4th rendering surface. A Kroma App compiled to wasm32 takes over an HTML <canvas> and renders its widget tree with the existing stack (winit-web → wgpu-WebGPU → Vello), routing the browser's pointer/keyboard events into app.dispatch exactly as run_app does on the desktop. The page becomes a thin shell; Kroma paints every pixel.
This reuses 100% of the platform-agnostic core — widget, view (view(state)reconcilemessage-loop), constraint layout, event routing, painter. Per [[stack-RFC-021-stack-graph-connections-glossary]]/RFC-021's Backend trait, the web target is a new Backend alongside HeadlessBackend (PNG) and WinitBackend (desktop window) — the architecture already anticipates it.
3. Viability — CONFIRMED at the dependency level
Every render dependency already targets wasm32: wgpu 22 (WebGPU backend), Vello 0.3 (web examples exist), winit 0.30 (web/canvas backend), skrifa (pure Rust). The only Kroma-side work is excluding the desktop/terminal deps from the wasm build. Audited (Cargo.toml + per-module grep, 2026-06-25):
- Wasm blockers (4 deps, all terminal/desktop-only):
portable-pty(PTY),arboard(clipboard),accesskit_unix(AT-SPI),accesskit_winit. - Modules with a direct desktop dep (3):
session,winit_backend,a11y. - Already web-safe (the whole app path):
widget,view,event,painter,verge_tokens,contrast,metrics,backend,app_runner— plus the pure terminal-logic modules (vt,scroll,search,selection,mouse).
Gating plan (DONE — spike 072a): the 4 desktop deps moved to a [target.'cfg(not(target_arch = "wasm32"))'.dependencies] block; the 3 desktop modules are #[cfg(not(target_arch = "wasm32"))]. The wasm build pulls the web-safe set + (072c) a new web_backend (winit-web canvas + wgpu WebGPU surface), with the form as the wasm-bindgen entrypoint.
PROVEN (spike 072b, 2026-06-25): cargo build --target wasm32-unknown-unknown
--lib compiles — libkroma.rlib built with wgpuvellowinit/skrifa all compiled for wasm32, first try, no patches. The compile gate (the dominant viability signal) PASSES.
4. Surface policy — canvas vs HTML (not either/or)
- Kroma/canvas for app surfaces behind login (dashboards, forms, editors)
where design fidelity + desktop↔web code reuse win and SEO is irrelevant.
- HTML (Koder Jet + templHTMX, per [[web-server]]) for *ubliclanding/
content*pages — SEO, link-ability, lightweight, progressive. Canvas content is opaque to crawlers (like an image); never put indexable/marketing content in it.
This mirrors how the Stack already splits product UIs (app vs landing).
5. Trade-offs & risks
- Faster? Worse time-to-first-paint for a trivial isolated form (wasm
download + GPU init). Smoother/consistent for rich, interactive, animated UIs. Net win is reuse + cross-browser pixel-fidelity, not raw speed for a 5-field form.
- WebGPU required. Vello uses compute shaders → needs WebGPU (no good WebGL2
fallback). Stable in Chrome/Edge, rolling in Firefox, Safari 18+. Fine for a controlled app audience; a constraint for old-browser long-tail.
- Accessibility. Canvas is invisible to assistive tech unless an a11y tree is
built. Kroma already uses AccessKit (terminal); AccessKit has a web/DOM adapter — well-positioned, but real work. Gate item (mirrors RFC-019 §4.2).
- Form ergonomics lost from native HTML: password-manager/autofill, browser
spellcheck, IME for some scripts, native context menus — must be re-provided.
- Security: wasm is obfuscation, not a boundary — it decompiles, and the
client is never trusted. Per [[security]]/always-on: the server validates. Wasm raises the bar against casual scraping/tamper; it is not a trust boundary.
- Download size: the winit+wgpu+vello wasm bundle is non-trivial (smaller than
Flutter-Web CanvasKit, still 100s of KB–MBs) — acceptable behind login, weigh for first-visit-critical flows.
6. Validation milestone (the spike) — RESULT (2026-06-25)
Compiles + loads + gets a WebGPU adapter (spike 072abc, dev-linux-kdb + headless Chromium):
cargo build --target wasm32-unknown-unknown --libcompiles (072b) — the fullrender stack (wgpuvellowinit/skrifa) built for wasm32, first try.
src/web_entry.rs(#[wasm_bindgen(start)]) +crate-type cdylib→wasm-bindgen produces a real browser module (
kroma_bg.wasm5.8 MB +kroma.js). Served + loaded in headless Chromium: the wasm runs and obtains a WebGPU adapter — WebGPU works end-to-end up to device creation.- One inch short:
requestDevicefails on a *gpu-0.22-vs-current-Dawn limitsskew*(
maxInterStageShaderComponents, removed from the WebGPU spec). NOT a Kroma issue — fixed by a vello+wgpu bump (pending/073; it ripples into the canonical desktop render, so it's a verified slice of its own). After that, the bundle renders the form to the canvas.
Net: viability is proven well past compile — the form runs in a real browser and talks to the real WebGPU stack; only a library version bump remains for pixels.
7. Relationship to RFC-019 & the Rust→Koda flip
This is the 4th Kroma surface (terminal → desktop window → .deb → web), fully consistent with RFC-019: it also sunsets Flutter Web for login-gated app surfaces. It survives the RFC-019 §9 Rust→Koda flip — when Koda gains the wgpu/ windowing equivalents, the web backend re-targets the same way the desktop one does.
8. Non-goals
- Not a browser-engine replacement. Page wasm in a browser (incl. Kruze, a
CEF browser) runs on V8;
kwasm(the self-hosted server/embedded wasm runtime, stack#155) is a separate axis and is explicitly NOT a V8/Kruze replacement. - Not for SEOpubliccontent pages — those stay HTML (§4).
- Not a security boundary (§5).