Plan 035 · Architecture · SpaceMusic · Draft

Headless UI technology evaluation

Three ways to build the user interface for a headless SpaceMusic — and the case for the one we think is right.

01 · What plan 034 left open

Plan 034 committed SpaceMusic to a future in which the program splits into two cooperating pieces: an engine that owns the 3D scene, the audio, and the plugin runtime, and a user interface that lives in its own process and can run either next to the engine on the same machine or remotely against it from a phone or tablet. What plan 034 deliberately did not answer is the next question: how is that user interface actually built?

Today, the user interface is written in Avalonia — a cross-platform .NET framework that paints native-feeling windows on Windows, macOS, Linux, and (with some effort) inside a web browser. The desktop version is mature and working. The browser version exists as a small wrapper project (SpaceMusic.Pro.Browser) that compiles the same C# code to WebAssembly — a low-level format that browsers can execute almost as fast as native code. The whole Avalonia user interface, plus the .NET runtime that supports it, gets shipped to the browser as a download.

That works. The question this document answers is whether it's the right long-term shape for the headless world, especially now that day-one mobile and tablet use is on the table — and what the realistic alternatives are.

This document evaluates three approaches side by side, applies them to SpaceMusic's actual constraints, and ends with a recommendation. The accompanying server infrastructure — streaming, real-time messaging, object storage, and single sign-on — is already running on our Hetzner machine and is described briefly where each option leans on it. Every acronym is defined in plain language on first use and again in the Glossary at the end.

02 · The three options at a glance

Each option is a different answer to a single question: how much code do we run inside the browser, and in what language? Everything else — the engine on Windows, the wire protocol carrying parameter changes back and forth, the video stream showing the rendered scene — is roughly the same across all three.

  1. Option A · Avalonia → WebAssembly. Ship the existing C# user interface to the browser unchanged. The .NET runtime, the Avalonia rendering engine, and our user interface code all download and run as WebAssembly — a format browsers can execute efficiently. Maximum code reuse; heaviest download.
  2. Option B · .NET WebAssembly for data only + JavaScript user interface. Compile just the data-syncing layer to WebAssembly (channels, the generated parameter model, message serialization), and write the visible user interface in JavaScript or TypeScript using a modern web framework. The user interface looks and feels native to the browser; the .NET part is a small library powering it from below.
  3. Option C · Pure web user interface. No .NET in the browser at all. The user interface is HTML, CSS, and JavaScript, generated from the same parameter spreadsheet (CSV file) that drives the desktop Avalonia user interface today. It talks to the engine over a WebSocket (a long-lived two-way text channel) and receives the rendered preview as a normal HTML <video> element fed by LiveKit, our streaming server.

Figure 1 · What runs in the browser for each option

WHAT RUNS IN THE BROWSER Option A Avalonia → WebAssembly Option B .NET data layer + JS UI Option C Pure web user interface Skia canvas in browser tab no DOM · custom drawing Avalonia · UI.Pro · UI.Core XAML · C# · same as desktop .NET runtime as WebAssembly Mono · Skia native · 25–40 MB WebSocket · LiveKit overlay video in DOM, z-stacked Engine (vvvv / Stride) DOM + native <video> browser-native rendering JavaScript UI · Solid / Svelte ~100–200 KB · DOM-rendered .NET data layer as WebAssembly channels · serializer · ~2–3 MB WebSocket · LiveKit video as DOM element Engine (vvvv / Stride) DOM + native <video> browser-native rendering JavaScript UI · Solid / Svelte ~100–200 KB · reads CSV manifest no .NET in browser WebSocket · LiveKit video as DOM element Engine (vvvv / Stride) LEGEND UI rendering layer supporting runtime / transport browser surface / engine

The shape of the trade-off is visible at a glance. Option A is one tall stack — everything is C#, including the renderer, but a lot of code has to be downloaded before anything appears. Option C is the shortest stack and the smallest download, but the user interface code is now JavaScript instead of C#. Option B sits in the middle: it keeps a small piece of .NET in the browser but writes the visible interface in JavaScript. The rest of this document is about which of those trade-offs is right for SpaceMusic given where it's headed.

03 · Option A · Avalonia compiled to WebAssembly

What it is. We already have SpaceMusic.Pro.Browser wired into the solution. It targets net8.0-browser and references our existing SpaceMusic.UI.Pro and SpaceMusic.UI.Core projects unchanged. Building it produces a folder of files (a ~25–40 megabyte Brotli-compressed bundle — Brotli is a modern compression format browsers understand natively) that any browser can load. Open the URL on a phone, the whole .NET runtime, the Avalonia framework, and our user interface code download and start running. From there it's the same code paths as the desktop application.

What's good about it

Almost all of our existing user interface code is reused. CsvPageView, CsvUiParser, SliderRow, LedToggle, CollapsiblePanel, the channel binding plumbing, the dark theme — all of it works without modification. A bug fix made for the desktop user interface automatically benefits the browser. There is one source of truth for visual styling. The team's day-to-day language stays C# and XAML (Avalonia's markup language for declaring user interface layouts). There is no new build language to learn, no new package manager, no new test framework.

Plan 034 also names this as the v1 recommendation for the standalone executable. Picking Option A keeps the desktop standalone and the remote tablet on a single codebase.

What's hard about it

The download is the central problem. Avalonia draws every pixel through its own copy of Skia — a graphics library — running inside the browser. Skia has no native HTML compositor underneath, so the browser is doing the work twice: once to host the .NET runtime and Skia, and once to compose Skia's output into the page. On a desktop with fast Wi-Fi and a cached visit, this is barely noticeable. On a phone, walking into a venue over LTE (4G mobile data) and scanning a QR code to bring up SpaceMusic for the first time, the user stares at a blank loading screen for ten to twenty-five seconds while ~25–40 megabytes of compressed bundle arrives and is parsed. The default heap configuration even refuses to start on iPad Safari and needs to be tuned by hand. Hover behavior leaks from desktop into touch, the virtual keyboard does not automatically scroll the focused input into view, and Avalonia's official virtual keyboard component is a paid commercial tier.

For LiveKit video, there is no .NET client library. The preview stream has to be rendered in a normal HTML <video> element that floats above Avalonia's canvas, which means coordinating the two render trees — z-order, fullscreen, hit-testing — by hand, in the same shape Unity's web export had to solve.

Avalonia is genuinely production-grade in 2026 — used by JetBrains, Unity, Schneider Electric, GitHub. Avalonia 12, released in April 2026, ships dramatic performance improvements (claims of up to 19× framerate gains) — but those gains are for native Android and iOS, not for the browser backend, which did not get a comparable rewrite. The browser story has not improved at the same pace as the rest of the framework, and there is no public roadmap commitment to close the gap.

Relative cost

The smallest of the three to a v1 that compiles and runs — Pro.Browser already does, so the delta is just plan-034 wiring and a mobile polish pass. The catch is what "v1" actually means. Just compiles-and-runs is trivially cheap. Reaching the polish required for actual mobile deployment — heap tuning for iPad Safari, hover-to-tap conversion, virtual-keyboard scroll wiring, LiveKit DOM overlay coordination, ahead-of-time compilation in CI, testing on a low-end Android phone and an older iPad — is several times that. The first number is technically correct but answers the wrong question; the second is the honest one if mobile is actually a requirement.

04 · Option B · .NET WebAssembly data layer plus a JavaScript user interface

What it is. Compile only SpaceMusic's data and synchronization layer to WebAssembly — the channel hub, the message serializer, the generated parameter model — and expose it to a JavaScript user interface through a thin interop bridge. The visible interface is written in TypeScript (JavaScript with type annotations) using a modern framework like SolidJS or Svelte. The .NET piece in the browser is a small library, not a complete user interface; it handles channel pub/sub, message types, and scene serialization.

What's good about it

Significant code reuse on the data side. SpaceMusic.Channels, the channel message wire format, and the generated parameter constants (~960 kilobytes of generated C# source compiled and trimmed) compile to WebAssembly without modification. Scene save/load logic could run client-side. The user interface itself gets the genuine benefits of being browser-native: real DOM (the browser's document model), real CSS, correct touch and virtual keyboard behavior, PWA (progressive web app — installable from the browser) capability for free.

What's hard about it

Three structural problems.

First, the WebAssembly runtime is not free. A minimal .NET 9 browser app still ships ~1.8–2.5 megabytes of compressed runtime before any of our code. Add the reactive extensions library, the channel code, and the generated parameter model, and a realistic first load is 2.3–3.2 megabytes. That's an order of magnitude smaller than Option A — but also an order of magnitude larger than Option C, which would only ship JavaScript.

Second, the JavaScript-to-WebAssembly boundary has per-call overhead measured in single-digit microseconds. For a slider being dragged at 60 hertz across 20 visible parameter rows, that adds a few milliseconds per frame of pure boundary cost. It's not catastrophic. But every channel write has to be marshalled across the boundary, with string encoding of the channel path, every frame. In practice, the JavaScript side could send the same message directly over the WebSocket with a one-line JSON.stringify — at which point the question becomes: what is the .NET data layer actually doing for us?

Third, our SpaceMusicMeta project — which holds the generated parameter model and would be the natural candidate to compile to WebAssembly — transitively references vvvv-specific types (VL.Core, VL.Lib.Collections.Spread<T>, Stride.Core.Mathematics) that don't support the browser target. Splitting it cleanly into a browser-safe "model" half and a vvvv-bound "runtime" half is real refactoring work. The csvSystem survey calls this out as the hidden cost the option's surface description understates.

The honest version of Option B is: most of what makes Option B attractive is also true of Option C, and most of what makes Option B different is friction. A small TypeScript spike would quickly clarify that the WebAssembly data layer is paying for itself only if scene save/load, undo/redo, and complex parameter records must round-trip identically across desktop and web. Plan 034 already plans for scene state to live on the engine — so the answer is probably no.

Relative cost

Roughly five to six times Option A's v1 cost — most of it is the SpaceMusicMeta split, the JavaScript-to-WebAssembly interop bridge, and standing up a generic parameter renderer in TypeScript. Reaching full parity is another large multiple on top of that, mostly because the user interface still has to be designed and built from scratch even though some .NET is kept. The pragmatic alternative this option keeps suggesting: skip the WebAssembly module, write the same ~800–1200 lines of TypeScript that would replace it, and you are essentially in Option C minus a small fraction of the work.

05 · Option C · Pure web user interface, generated from the CSV

What it is. No .NET in the browser. The user interface is HTML, CSS, and TypeScript, built with a modern framework — SolidJS or Svelte are the two best fits. The parameter rows are generated at build time from the same Pro UI CSV file that already drives the Avalonia user interface. Our existing code generator (SMCodeGen) gains a small new emitter that writes a TypeScript module of the parameter hierarchy alongside the C# files it already emits — same single source of truth, two consumers. The CSV remains the only place parameter definitions are authored.

The user interface talks to the engine over a WebSocket, using the existing channel message format ({p: path, t: type, v: value} — already plain JSON, already language-neutral). For remote use it goes through Centrifugo, the WebSocket relay running on our Hetzner server. For local use it talks directly to the engine's local WebSocket. The preview video stream is a normal HTML <video> element fed by LiveKit. File uploads from a phone go directly to our existing MinIO storage (S3-compatible object storage running on our Hetzner box) over presigned URLs — covered in detail in §07.

What's good about it

The mobile experience is, by some distance, the best of the three. Touch, momentum scroll, native virtual keyboards, the system file picker, pull-to-refresh, installable as a home-screen app, OAuth redirects in a real browser tab — all of this is the platform's native behavior, not something we emulate. The cold-start budget is roughly two orders of magnitude smaller than Option A: a SolidJS or Svelte 5 application of this complexity initial-loads in 100–200 kilobytes of JavaScript, plus 10–30 kilobytes per route fetched lazily. On a mid-range Android phone over 4G, the interface is interactive in well under two seconds.

Performance under SpaceMusic's actual workload — many small reactive values changing many times a second — is also where modern signal-based frameworks shine. SolidJS and Svelte 5 update only the exact DOM nodes whose subscribed value changed, with no virtual-DOM reconciliation in between. A virtualized list of 500 parameter rows runs smoothly at 60 hertz on a mid-range phone using off-the-shelf libraries (TanStack Virtual, Virtua). This is the cheapest path through the perf budget plan 034 sets for the user interface (adaptive 30–60 hertz).

Server SDKs (software development kits — the libraries that let a client talk to a service) are mature in JavaScript in 2026 in a way they aren't always in C#: the LiveKit JavaScript client is the canonical implementation, centrifuge-js is the official Centrifugo client, the modular AWS SDK for S3-compatible storage is well-tuned. We get to use the best-resourced clients for the services we already run.

What's hard about it

It's the largest rewrite of the three. Roughly 4,900 lines of C# and 1,100 lines of XAML in UI.Pro, plus substantial parts of UI.Core (the file loader, the number-pad input, the overlay system, the slider/toggle/dropdown row implementations) get rewritten in TypeScript. The CSV stays, the channel protocol stays, the engine stays — but the visible interface is new code.

The original critique of Option C in our internal review was "two renderers forever" — Avalonia desktop for the in-studio standalone executable, plus the new web user interface, both maintained in lockstep. That critique disappears if the standalone executable becomes a thin native shell around the same web user interface. Specifically: WebView2 — Microsoft's embeddable Chromium-based browser component, ~5 megabyte shell overhead — can host the web user interface inside a normal Windows window, talking to a locally-running engine over the same WebSocket the remote case uses. The standalone is the web user interface, wrapped. There is only one renderer codebase. The Avalonia user interface can be retired.

This is the framing we recommend in §06.

Other real risks: WebRTC encode/decode adds 30–80 milliseconds of latency over the local Spout zero-copy path that plan 034 calls out for in-studio operators dragging on the preview pane. For pure parameter editing this is invisible; for direct manipulation of the rendered scene with the mouse it is perceptibly worse than embedded Avalonia plus Spout. We address this in §06.

The channel transport also has known gaps: the current Centrifugo provider stubs out enum-entry serialization (dropdowns arrive empty on the remote side), spread instance counts aren't observable, and some parameter record types aren't yet serialized. These gaps exist regardless of UI option but become visible first under C because there is no in-process channel hub to fall back on.

Relative cost

The largest of the three. v1 — a single-page parity with the current Pro UI main view (Objects, Position, MIDI, Global panels; the standard control vocabulary; one preview stream; local + relay transports; dark theme; phone-friendly layout) — is roughly seven to ten times Option A's compiles-and-runs baseline, and a small multiple on top of Option B. Full parity (every minor control kind, dynamic-instance add/truncate, scene save/load dialogs, the detail-overlay modal pattern, settings, the file library browser, accessibility) is another large multiple again. Per-plugin custom UIs would be additional, but SpaceMusic currently has one (first-party) plugin, so the plugin SDK is not a v1 concern.

06 · The comparison and the recommendation

The three options sit on a clear trade-off curve. Reuse and bundle size move together; mobile quality and rewrite cost move together inversely. The four constraints that shape the decision — mobile is day-one critical, the standalone executable can be a WebView2 shell rather than Avalonia, the team is fluent in modern web stack, no fourth hybrid option in scope — push hard in one direction. Here is the side-by-side, then the recommendation.

Criterion Option A Option B Option C
Code reuse from current Avalonia UI ~95%. Pro.Browser already compiles against UI.Pro/UI.Core unchanged. ~40–50%. Channels + generated model reused; UI rewritten; SpaceMusicMeta needs splitting. ~5%. Only the CSV survives. UI rewritten in TypeScript.
Mobile / phone experience Rough. Hover leaks into touch; virtual-keyboard scrolling manual; iPad Safari heap must be tuned by hand. Strong. Native DOM = correct touch, scroll, virtual keyboard, PWA installable. Best of the three. Touch, momentum scroll, share sheet, OAuth, PWA install all first-class.
First-load size (Brotli-compressed) 25–40 MB (and 50–95 MB uncompressed in browser memory). 2.3–3.2 MB. ~500 KB realistic, including LiveKit, Centrifugo, and AWS S3 client.
Cold start on mid-range Android over 4G 5–10 seconds AOT-compiled; 10–25 seconds first visit on iPad Safari. 1–2 seconds; UI shell renders before runtime finishes loading. 0.5–1.5 seconds to interactive.
LiveKit integration No .NET SDK. Bridge to JavaScript SDK; coordinate <video> overlay over Skia canvas. Native JavaScript SDK in the shell. Native JavaScript SDK; <video> as a normal DOM element.
Iteration speed (designer / developer) Multi-second WebAssembly relink per change; .NET debugger in browser is rough. Vite hot-reload on JS side; .NET side still slow. Vite hot-reload sub-100 ms. Best developer experience by some distance.
Accessibility (screen readers, EU EAA) Canvas-rendered Skia surface is opaque to screen readers. Native DOM; standard ARIA support. Native DOM; standard ARIA support; can audit with Lighthouse / axe-core.
Standalone .exe under plan 034 Avalonia desktop (plan 034's v1 recommendation). Avalonia desktop kept; web UI parallel. WebView2 shell around the same web UI. One renderer codebase.
Relative effort to v1 Smallest if you count compiles-and-runs (baseline 1×). For honest deploy-to-mobile, several times that. Roughly 5–6× baseline. SpaceMusicMeta split + interop bridge + JS framework setup. Roughly 7–10× baseline. Full rewrite of the rendering layer.
Relative effort to full parity Adds a small multiple on top of v1. Vendor-roadmap dependent for mobile. Adds a similar multiple as v1 itself. Plugin contract + design tokens pipeline. Adds a similar multiple as v1 itself. Full feature parity with the Avalonia UI.
Risk that underlying tech doesn't deliver Medium-high. Browser backend rewrite not on Avalonia's announced roadmap. Medium. WebAssembly data layer at risk of being a leaky middleman. Low. SolidJS / Svelte / Vite / LiveKit-JS all production-grade and widely deployed.

The shortest version: Option A is the cheapest path that doesn't work for mobile. Option C is the longer path that does. Option B is the path that, examined honestly, keeps suggesting we should just commit to Option C.

Recommendation: Option C, with the standalone .exe as a WebView2 shell

The three answers that came back from the planning conversation — mobile is day-one critical, a WebView2-wrapped standalone is acceptable, the team is fluent in modern web — turn what was a balanced three-way comparison into a clear direction. Mobile day-one alone is enough to disqualify Option A from the v1 use case: the cold-start budget for a phone over LTE is not negotiable with a 25–40 megabyte WebAssembly bundle, and Avalonia's browser backend is not on a roadmap that will close the gap. Option B, examined past its surface description, mostly inherits the rewrite cost of Option C while adding the WebAssembly-runtime overhead and an interop bridge whose value collapses if scene state lives on the engine — which plan 034 says it does.

Option C lands cleanly. With WebView2 hosting the same web user interface as the standalone executable, the "two renderers forever" objection is replaced by "one renderer, two shells": the same TypeScript code runs inside WebView2 on the operator's studio machine, and inside any browser anywhere else. The Avalonia desktop user interface continues to exist inside vvvv itself (where it's embedded today, not as a standalone shell), and over time can be reduced in scope to the in-engine editing surface, freeing the standalone case entirely. The team's fluency in the modern web stack converts Option C's larger up-front scope into faster real-world iteration: sub-100 millisecond hot reloads in Vite compound across the life of the project in a way no WebAssembly relink loop can match.

The honest counterweights: the largest rewrite of the three, and Avalonia's investment in the desktop user interface is partially retired rather than fully reused. Both are real. Neither is bigger than the cost of trying to make Avalonia WebAssembly hit phone-class targets.

The in-studio preview latency caveat

Plan 034 specifies that an in-studio operator dragging on the preview pane should round-trip mouse events within one Stride frame (4–16 milliseconds) via the Spout zero-copy texture path. WebRTC encode + decode adds 30–80 milliseconds on top of that, even on the same machine. For pure parameter editing this is invisible; for direct mouse-manipulation of the rendered scene, it is noticeably worse. Two paths through this:

For v1 we suggest the first; the second is a candidate for a later plan.

07 · Remote asset loading

One specific use case shapes a lot of the remote user-interface design and deserves its own section: an operator wants to load a file that's on their phone — say, a photograph they just took — into a SpaceMusic plugin running on the engine machine in a different room. The current desktop user interface has a file picker that browses the engine machine's local filesystem. The remote case needs three picker modes side by side: browse the engine's SpaceMusic library, browse the engine's filesystem (within a safe allowlist), and pick a file from the device the user interface is running on. The third mode is the new one.

The shape of the solution leans entirely on infrastructure we already run. Our server has MinIO at storage.spacemusic.tv — an object storage service that speaks the same protocol as Amazon S3. Object storage gives us presigned URLs: a short-lived signed URL that grants the holder permission to upload (or download) one specific file directly, without going through our API. The pattern is fifteen years old and rock-solid; every modern browser and framework has built-in support.

The flow:

Figure 2 · Loading a file from a remote phone into a plugin on the engine

REMOTE UPLOAD FLOW · PHONE → MINIO → ENGINE Phone browser API gateway api.spacemusic.tv MinIO storage.spacemusic.tv Engine vvvv on Windows Plugin FileLoader POST /presigned (PUT) 1 · request URL URL · 1 hour TTL 2 · signed URL PUT · file bytes direct 3 · upload (bypasses API) RemoteUploadCompleted (channel) 4 · notify engine over WebSocket GET object 5 · engine pulls from storage file bytes 6 · cache locally *File = local cache path 7 · write to *File channel Plugin loads file from normal local path 8 · plugin unchanged LEGEND file-bytes path (high bandwidth) control / metadata

Four properties of this flow matter for SpaceMusic's plugin architecture.

  1. The plugin is unchanged. Every existing FileLoader plugin sees a normal local filesystem path on the engine machine, just like today. The fact that the file came from a phone in another room is invisible past the channel boundary — preserving the "channels are the source of truth" invariant the codebase already follows.
  2. The engine never proxies the upload. The phone uploads directly to MinIO over the operator's network. The engine only fetches bytes it actually needs, when it needs them. This matters when the engine machine and the operator are on different networks.
  3. The picker has three tabs. "Engine library" and "Engine filesystem" reuse a small new read-only endpoint on the API gateway, restricted to an allowlist of operator-blessed folders (no arbitrary path traversal). "My device" is a plain HTML <input type=file accept="image/*" capture="environment"> — which on a phone opens the native camera/photos/files picker without any custom code.
  4. Cleanup is automatic. MinIO buckets already have lifecycle policies for the existing recordings/textures/assets/exports/backups buckets (30–90 day expiry); adding a 7-day policy on a remote-uploads/ prefix means orphaned phone uploads delete themselves.

The pattern is transport-agnostic across the three user-interface options — Option A would still need to use it (Avalonia's storage provider does not reach MinIO directly in the browser). The difference is only in who writes the picker chrome. Under Option C the picker is plain HTML and about 150 lines of TypeScript.

Other server services we already run

Beyond MinIO, the user-interface stack leans on infrastructure that's already in production on the Hetzner box, all of which has mature JavaScript clients:

None of these are new for plan 035. They are already what plan 034 was going to use. The point is that Option C is the path that talks to them most naturally — the canonical JavaScript clients for LiveKit, Centrifugo, and S3-compatible storage are all official, well-resourced, and what the rest of the web ecosystem standardizes on.

08 · What we don't yet know

Honest disclosure: the bundle sizes and cold-start numbers in this document are extrapolated from reference applications and 2026 framework benchmarks, not measured against our actual code. Pro.Browser pulls in our specific dependencies (Avalonia.Svg.Skia, LucideAvalonia, CommunityToolkit.Mvvm) that may push the real number higher or lower than the published references.

A small measurement spike would resolve most of this:

  1. Publish SpaceMusic.Pro.Browser with ahead-of-time compilation and Brotli compression enabled. Weigh the actual output.
  2. Serve it over the existing devpush pipeline; cold-start it on a real phone over an LTE hotspot. Measure time-to-interactive.
  3. Open the parameter panel and scroll a long list. Note whether it stutters.
  4. Repeat on a budget Android phone (a Samsung Galaxy A15 5G is the recommended 2026 low-tier baseline) and a 4-year-old iPad.

The result could materially shift the picture in either direction. If the bundle is actually closer to 15 MB and cold-start on a phone is under 5 seconds, Option A becomes a more honest contender than the framework benchmarks suggest. If it's at the 40 MB ceiling and time-to-interactive is over 15 seconds, Option C's case is even stronger than this document already makes it.

Two further things we don't know that would affect a real deployment: the bandwidth budget for WebRTC TURN relay (the protocol that punches through hostile NATs on venue Wi-Fi — needed when the operator's phone is on a different network than the engine), and whether accessibility compliance under the EU European Accessibility Act (in force from June 2025) is required for any production deployment. The first affects operational costs; the second is binary, and would disqualify Option A entirely if it applied.

09 · Open questions for the next decision pass

This document answers "which of A, B, or C" — not the full sequence of decisions that follow if Option C is picked. The questions below are the ones that materially shape the next plan.

  1. Web framework. SolidJS, Svelte 5, Lit, or React + TanStack? SolidJS is the best technical fit for the channel-binding pattern (fine-grained signals map almost 1:1 onto reactive channels); Svelte 5 is very close on signals and has better default tooling and templating; Lit is the most plugin-friendly via Web Components; React is the safest hire-for-talent default but the heaviest at runtime. Decide before any v1 code lands.
  2. WebView2 packaging. Does the standalone .exe ship as a code-signed installer (MSIX, Inno Setup, signed installer)? Does that signing pipeline exist? WebView2 itself is a Microsoft-distributed runtime that may need an installer prerequisite check.
  3. Preview-interaction architecture. Keep an embedded vvvv user interface for the in-studio operator (preserves Spout zero-copy mouse round-trip), or move preview interaction onto the engine and have all user interfaces send only normalized event coordinates? The first is the easier v1; the second is the cleaner long-term shape.
  4. Visual design contract. Pixel-perfect parity between the new web user interface and the existing Avalonia desktop, or use the web rewrite as an opportunity for a mobile-first redesign? If pixel-perfect, a shared design-tokens pipeline (one JSON of colors/spacing/typography → both Avalonia resources and CSS variables) is required. If redesign, divergence is intentional and the design system is web-native.
  5. Channel-protocol gap closure. The current Centrifugo provider stubs out enum-entry serialization, spread instance counts, and parameter record types. Close these gaps as the first sprint of plan 036 (before any user-interface work), so the web user interface has a faithful contract to render against.
  6. Plugin user-interface contract. SpaceMusic today has one user-interface plugin (the first-party Pro UI). If a third-party plugin ecosystem is on the roadmap, the user-interface contract — how a plugin ships its own controls to the web user interface — needs to be designed up-front. If not, the question can wait. Decide intent now; design later only if intent says yes.
  7. Measurement spike. Run the spike from §08 before committing to a direction. Confirm or revise the cold-start and bundle assumptions this document is built on.

10 · Why this matters

"The right user-interface technology for SpaceMusic in 2026 is the one whose biggest weakness — engineering effort to v1 — we can pay for once, instead of the one whose biggest weakness — mobile experience — we'd be apologizing for forever."

Plan 034 set up SpaceMusic to be controllable from anywhere. Plan 035 is the choice that decides what "anywhere" actually feels like — what an operator's phone, a tablet next to the stage, a laptop in another room, all experience when they connect to the engine. Picking Option C with a WebView2 standalone shell makes that experience the same as every other modern web application the operator already uses every day: it opens fast, it feels native to the device, it works on a slow connection, it installs to the home screen, and it gets out of the way. Picking Option A optimizes for code reuse and gets a desktop application running in a browser tab — which is what we have today and which is exactly what's not working for the future plan 034 describes.

The cost is real: substantially more work than Option A to fully replace what Avalonia ships today, plus the discipline to keep the channel protocol and CSV contract clean across two consumers (desktop vvvv and web). The infrastructure investment — LiveKit, MinIO, Centrifugo, Authentik, the API gateway — is already in place. The CSV remains the one source of truth. The standalone executable becomes a 5 megabyte native shell around the same code that runs on a phone. There is one renderer, one design system, one place to fix a bug.

If we say yes, the next plan (036) is the implementation roadmap: framework pick, channel-gap closure, design-token pipeline, first slice of the user interface against the existing engine, the WebView2 shell, deploy. If we say no — if the measurement spike comes back with surprisingly good numbers, or if mobile turns out to be less critical than we currently think — Option A's near-zero path to a runnable v1 is still on the table and nothing in this document forecloses it.

Glossary

Acronyms and product names used in this document, in plain language.

AOT
Ahead-of-Time compilation. Translating .NET code to native machine code at build time, instead of at first run. Produces faster startup and runtime performance at the cost of larger bundles and slower builds.
API
Application Programming Interface. A defined set of calls a program exposes for other programs to use. In this document usually refers to the SpaceMusic REST API at api.spacemusic.tv.
ARIA
Accessible Rich Internet Applications. Web standard for marking up interactive elements so screen readers and other assistive technology can describe and operate them.
Authentik
Open-source single sign-on (SSO) server. Holds one user account per person; all SpaceMusic services authenticate through it.
Brotli
A modern compression format (~20% smaller than gzip for text/code). All evergreen browsers decode it natively. Web servers serve .br-compressed files directly.
Centrifugo
A self-hosted WebSocket relay server. Brokers pub/sub messages between many clients; we run it at relay.spacemusic.tv.
CSV
Comma-Separated Values. A flat text-file table format. SpaceMusic's parameter definitions live in CSV files authored from Google Sheets.
DOM
Document Object Model. The browser's in-memory tree representing the HTML document. Native HTML/JavaScript renders into the DOM; Avalonia in the browser bypasses it and paints to a canvas.
EU EAA
European Accessibility Act. EU regulation requiring digital products and services to be accessible to people with disabilities. In force from June 2025.
HMR
Hot Module Reload. Development feature where saving a source file updates the running application in the browser without losing state — used in Vite and similar JavaScript bundlers.
JIT
Just-In-Time compilation. Compiling code at first execution rather than at build time. Generally smaller bundles than AOT but slower startup.
JWT
JSON Web Token. A small signed credential format used for authentication. Used by Centrifugo to authorize WebSocket connections.
LiveKit
Open-source WebRTC streaming platform. Handles real-time audio/video between many participants. We run it at stream.spacemusic.tv.
LTE
Long-Term Evolution. The 4G mobile data standard. Commonly available worldwide; bandwidth depends on local network conditions.
MinIO
Self-hosted S3-compatible object storage server. Stores arbitrary files (recordings, textures, assets, uploads) and serves them over HTTP. We run it at storage.spacemusic.tv.
NAT
Network Address Translation. The technique that lets many devices share one public internet address. Hostile NATs (often on captive Wi-Fi) require a TURN relay to allow WebRTC traffic through.
presigned URL
A short-lived signed URL granting the holder permission to upload or download one specific file from object storage, directly, without going through an API. The standard browser-to-S3 upload pattern.
PWA
Progressive Web App. A web application that can be installed from the browser to the home screen of a phone or desktop, working largely like a native app.
Skia
A graphics library originally from Google, used by Chrome to draw web pages and by Avalonia to draw user interfaces. Embedded inside the Avalonia browser bundle.
SDK
Software Development Kit. A library or set of libraries that lets a client program talk to a service. We compare JavaScript SDKs vs C# SDKs for each option.
SolidJS
A modern JavaScript user-interface framework with fine-grained reactivity. The ~7 KB runtime and signal-based update model make it well-suited to high-frequency reactive workloads.
Spout
A Windows-only library for sharing GPU textures between processes with zero copy. Used in plan 034 for local UI ↔ engine texture transport, achieving sub-frame latency.
SSO
Single Sign-On. One login for many services. Provided here by Authentik.
Svelte
A modern JavaScript user-interface framework. Version 5 introduced "runes" — signal-based reactivity similar to SolidJS. ~10–15 KB compiled.
TURN
Traversal Using Relays around NAT. A server that relays WebRTC traffic when direct peer-to-peer fails (hostile NATs, restrictive firewalls). Bandwidth on the TURN server is an operational cost.
TypeScript
JavaScript with type annotations. Catches many errors at build time that plain JavaScript only catches at runtime. The standard for serious modern web codebases.
Vite
A modern JavaScript build tool. Provides sub-100ms hot-module reload during development and produces small optimized bundles for production.
WASM
WebAssembly. A low-level format that browsers can execute almost as fast as native code. Lets languages other than JavaScript run in the browser. Options A and B both use WebAssembly to ship .NET code to the browser.
WebRTC
Web Real-Time Communication. The browser standard for peer-to-peer audio, video, and data channels. LiveKit is built on top of WebRTC.
WebSocket
A long-lived two-way text channel between a browser and a server. The protocol Centrifugo uses to deliver channel messages in real-time.
WebView2
Microsoft's embeddable Chromium-based browser component for Windows applications. Lets a native Windows .exe host a web user interface inside its own window with ~5 MB of shell overhead.
WHIP
WebRTC-HTTP Ingestion Protocol. A standard for ingesting a real-time stream into a WebRTC server. Used by the engine to push preview surfaces into LiveKit.
XAML
Markup language used by Avalonia (and WPF, MAUI) to declare user-interface layouts. The browser doesn't natively understand it; Avalonia renders it into Skia.

Settled by this plan

Recommendation: Option C + WebView2 shell

Pure web user interface generated from the existing CSV. Standalone executable is the same web UI hosted in a small WebView2 native shell. One renderer codebase. Avalonia desktop UI retires over time.

Next step

Measurement spike

Publish Pro.Browser AOT + Brotli, weigh it, cold-start it on a real phone over LTE. Confirm or revise the assumptions this document is built on before committing engineering capacity.

Later

Plan 036 — implementation roadmap

If recommendation holds: framework choice, channel-protocol gap closure, design-token pipeline, first slice of UI against existing engine, WebView2 shell, deploy.