stack-RFC-022 — Kroma Web Backend: the declarative UI on a browser canvas (WebGPU)

draft

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 corewidget, 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 --lib compiles (072b) — the full

    render 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.wasm 5.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: requestDevice fails on a *gpu-0.22-vs-current-Dawn limits

    skew*(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).