Skip to content

Proposal: migrate Buzz Desktop renderer to Native SDK + Zig #2960

Description

@marcusschiesser

Motivation

Buzz Desktop currently combines a Rust/Tauri backend with a React + TypeScript + Vite renderer running in the platform WebView. I would like to propose an incremental migration of the primary desktop renderer to Vercel Labs Native SDK, using a Zig application core while retaining the existing Rust backend and the relay as the source of truth.

The expected benefits are:

Native SDK's TypeScript core also compiles to native code and does not ship a JavaScript runtime. The reason to prefer Zig here is therefore not "removing Node"; it is to make memory ownership, performance-sensitive timeline work, Native SDK integration, and the narrow C ABI to Rust explicit. This should remain an incremental, measured migration because Native SDK is pre-1.0 and Buzz has a broad desktop feature surface.

I searched the open issues and did not find an existing Native SDK/Zig renderer migration proposal. The WebKit-related issues linked above are related motivation, not duplicates.

Phase-0 feasibility result

I implemented a contained renderer spike against Buzz c2a4ee711e48 and Native SDK ea98365a2d08:

The spike has a Zig-owned Native SDK shell, sidebar, composer, separately positioned WebView island, and a trailing-anchored variable-height timeline over 10,000 deterministic messages. A small Rust/Tokio static library emits typed events through a C ABI into Native SDK's event channel. Seven behavioral tests cover virtualization, scroll/prepend stability, input, send, channel selection, source events, and WebView placement.

Linux renderer-proxy results:

Workload (p50 / p90) Native SDK + Zig Current Buzz React renderer in headless Chromium Current / Native p50
First usable frame 2.43 ms / 4.30 ms 1.10 s / 1.52 s 453.74x
Scroll event → frame 1.30 ms / 1.53 ms 6.10 ms / 7.20 ms 4.67x
Prepend 50 → frame 1.06 ms / 1.10 ms 69.60 ms / 140.40 ms 65.72x
Composer send → frame 1.07 ms / 1.31 ms 53.90 ms / 79.70 ms 50.14x
Source event → frame 1.28 ms / 1.41 ms 67.90 ms / 69.50 ms 53.17x

Native realized 12 message rows / 124 widgets from the logical page, while the web run mounted 50 rows / 3,304 DOM nodes. The renderer-core harness peaked at 14.1 MiB RSS and produced a 3.9 MiB stripped ELF; the Chromium process tree measured 922.1 MiB with a 177.1 MiB JS heap, and the current E2E web distribution was 19.5 MiB.

These are directional feasibility numbers, not shipped-app claims. Native used Native SDK's deterministic CPU pixel renderer without GTK window startup or its final blit. Current Buzz used the production React bundle in headless Chromium with an in-page E2E relay/Rust stand-in; its 10,000-message mock lives in JS, so its first-frame and memory values are upper bounds. The real GTK4/WebKitGTK/Xvfb smoke test included in the repository must pass before Phase 1, and later phases need release-like measurements on Linux, macOS, and Windows.

The useful conclusion is narrower than the ratios: the architecture seam works, scrolling and mutation stay comfortably inside a frame in the renderer harness, and a small vertical slice is justified if the real host smoke passes.

Target architecture

                                  Buzz relay
                              (unchanged source of truth)
                                         │
                                  WebSocket / REST
                                         │
┌────────────────────────────────────────▼─────────────────────────────────────┐
│ Existing Rust desktop backend                                                │
│ identity/keyring · Nostr/network · caches/store · media · agents · updater   │
│ async work and durable/backend ownership remain here                         │
└────────────────────────────────────────┬─────────────────────────────────────┘
                                         │
                     narrow, versioned C ABI + bounded event queue
                     commands ↓                         events/results ↑
                                         │
┌────────────────────────────────────────▼─────────────────────────────────────┐
│ Zig application core                                                        │
│ Model · Msg · update · effects · subscriptions · presentation state          │
├──────────────────────────────────────────────────────────────────────────────┤
│ Native SDK renderer                                                         │
│ shell · navigation · virtualized timeline · composer · native surfaces       │
│ optional WebView-island coordinator for browser-dependent features           │
└─────────────────────────────┬────────────────────────────────────────────────┘
                              │
              ┌───────────────┼──────────────────┐
              ▼               ▼                  ▼
       macOS host          Linux host         Windows host
      AppKit/Metal       GTK4/software          Win32
       WKWebView*         WebKitGTK*          WebView2*

      * only for explicitly retained/migration WebView islands

Data-flow rules:

  1. Native input becomes a Zig Msg; update is the only place presentation state changes.
  2. Side effects issue typed commands over the ABI. Rust owns async execution, cancellation, secrets, networking, storage, and backend resources.
  3. Rust publishes versioned result/event batches into a bounded queue; a Native SDK subscription drains it on the UI loop. Rust never mutates UI state or invokes UI code from arbitrary worker threads.
  4. The ABI uses fixed-width/POD types, opaque handles, explicit ownership/free functions, schema/version negotiation, and no allocator ownership crossing the boundary.
  5. Zig receives windowed/derived view models rather than duplicating the entire Rust store. The relay and existing Rust data layer remain authoritative.
  6. WebView islands are presentation-only and use a typed message protocol. They do not become a second backend or a second source of state.
  7. The React renderer remains available behind a build/runtime switch until parity and rollout gates are met.

Implementation plan

Phase 0 — Feasibility spike (completed, with one host validation pending)

  • Pin exact Buzz, Native SDK, and Zig revisions.
  • Build a Zig-owned Native SDK model/update/view loop.
  • Prove Rust/Tokio → C ABI → Native SDK event delivery.
  • Implement a 10,000-message, variable-height, trailing-anchored virtual timeline.
  • Preserve logical row identity and viewport position while prepending history.
  • Exercise sidebar/channel selection, native composer/send, async source events, and WebView placement.
  • Add seven deterministic behavioral tests.
  • Add a reproducible Linux runner and compare matching user-visible workloads.
  • Publish source, raw results, methodology, and limitations.
  • Run the supplied GTK4/WebKitGTK/Xvfb smoke on a host with the required development packages and verify the real window, software blit, WebView island, automation snapshot, input, and IME.

Exit gate: maintainers agree the seam is worth a vertical slice; the real Linux host smoke passes; no rewrite commitment is made from renderer-proxy ratios alone.

Phase 1 — Foundations and parallel native target

  • Add a parallel desktop/native/ target; do not replace the production renderer.
  • Pin Native SDK and Zig versions reproducibly and document the update policy.
  • Integrate Zig/Native SDK builds into just, local setup, Hermit/tooling, and CI caches.
  • Create a small Rust bridge crate/library rather than exposing arbitrary Tauri commands directly.
  • Specify ABI v1: lifecycle, command envelope, result/event envelope, error model, cancellation, string/byte slices, opaque handles, and ownership/free rules.
  • Generate or validate matching Rust/Zig ABI declarations and add size/alignment/round-trip tests.
  • Add bounded queue behavior, backpressure/coalescing rules, and shutdown ordering.
  • Wire startup, session restore, backend readiness, reconnect, and graceful shutdown.
  • Establish the Zig Model / Msg / update / effects module boundaries and allocation strategy.
  • Add Native SDK design tokens matching Buzz themes and a minimal app shell.
  • Keep the existing React renderer selectable through a build flag or launch flag.
  • Add deterministic Native SDK tests, accessibility snapshots, reference screenshots, and ABI contract tests to CI.
  • Produce unsigned development artifacts for Linux, macOS, and Windows.

Exit gate: the native shell starts and stops the real Rust backend on all three desktop platforms, restores a session, exchanges commands/events without leaks or thread violations, and can switch back to the React renderer.

Phase 2 — Read-only channel vertical slice

  • Implement window chrome, community rail, channel/sidebar navigation, loading/empty/error states, and theme switching.
  • Expose windowed channel/message view models from Rust; avoid copying the complete message store into Zig.
  • Implement the variable-height virtual timeline with stable keys, tail following, backfill, viewport preservation, jump-to-latest, and unread boundary behavior.
  • Render core message forms: text, Markdown subset, author/time metadata, replies, reactions, status/system events, images/files, and safe link opening.
  • Use a WebView island temporarily for unsupported rich Markdown/code blocks if necessary, with explicit sizing and focus ownership.
  • Implement selection, focus traversal, keyboard navigation, context menus, copy, link activation, HiDPI scaling, reduced motion, IME composition, and screen-reader labels.
  • Cover long messages, mixed row heights, deleted/edited events, rapid live events, reconnect/backfill, and 100k-row stress behavior.
  • Run parity screenshots and interaction benchmarks against the current renderer on release-like Linux, macOS, and Windows builds.

Exit gate: a dogfood build can sign in, select communities/channels, read live and historical messages, and navigate a large timeline with correct accessibility and no unbounded mounted-row or memory growth.

Phase 3 — Messaging write path

  • Implement composer editing, multiline selection, keyboard shortcuts, draft persistence, and send/cancel/error states.
  • Implement replies/threads, mentions/autocomplete, reactions, edit/delete, typing, presence, unread/read markers, and optimistic reconciliation.
  • Implement attachment selection, upload progress/cancel/retry, drag-and-drop, clipboard paste, image/file preview, and safe download/open.
  • Preserve current identity/signing and publication semantics in Rust; Zig sends intents, not raw secret-bearing operations.
  • Handle offline queues, reconnects, duplicates, out-of-order events, rejected publications, and permission failures.
  • Port desktop E2E scenarios for the common messaging paths to Native SDK automation/record-replay.
  • Add failure-injection tests around every ABI command and event stream.

Exit gate: common channel and DM messaging workflows have behavioral parity, including offline/reconnect and failure states, and pass keyboard, IME, accessibility, and performance gates.

Phase 4 — Remaining surfaces and deliberate WebView islands

  • Inventory every current route, dialog, overlay, command, shortcut, and platform capability in a parity matrix.
  • Migrate Home/activity, DMs, search, profiles/people, onboarding, settings, community/channel administration, and account flows.
  • Migrate agent, team, workflow, project, repository, review, canvas, and job/handoff surfaces.
  • Port notifications inside the app, toasts, command menus, popovers, modals, and global navigation.
  • Classify browser-dependent features such as WebRTC/huddles, advanced audio, rich code/diff rendering, and any editor surface:
    • implement natively where Native SDK/platform APIs meet parity;
    • otherwise keep a bounded WebView island with a typed protocol, explicit lifecycle/focus/accessibility rules, and isolated assets.
  • Remove React modules route-by-route only after the corresponding native surface reaches parity.
  • Track every retained WebView island as an explicit architectural exception with an owner and follow-up decision.

Exit gate: the parity matrix is complete, remaining WebView dependencies are intentional and documented, and no WebView island owns durable/backend state.

Phase 5 — Platform integration, packaging, and quality

  • Integrate app/window lifecycle, native menus, context menus, tray, notifications, badges, deep links, single-instance handling, clipboard, file dialogs, drag-and-drop, and URL/file opening.
  • Integrate secure storage/keyring, updater, logging/crash reporting, permissions, proxy/network configuration, and platform-specific process launching through Rust or guarded Native SDK capabilities.
  • Validate huddles/media: microphone/camera permissions, device selection, audio focus, suspend/resume, sleep/wake, and background behavior.
  • Complete macOS signing/notarization/universal packaging, Linux AppImage/deb packaging, and Windows signing/installer/update flows.
  • Test native host and retained WebView behavior across supported OS/GPU/display configurations.
  • Add leak/soak tests, idle CPU checks, allocator statistics, crash recovery, malformed-event fuzzing, and ABI sanitization where supported.
  • Establish release performance budgets for cold/warm usable frame, scroll/prepend/input p90, idle CPU, RSS, mounted rows/widgets, and artifact size.
  • Run deterministic UI, accessibility, visual-regression, E2E, upgrade/downgrade, and packaging tests in CI.
  • Update contributor, debugging, architecture, testing, and release documentation.

Exit gate: signed/release-like builds pass the full platform matrix and soak tests, meet agreed performance/accessibility budgets, and can upgrade without losing identity, drafts, settings, or user data.

Phase 6 — Staged rollout and cutover

  • Ship an opt-in native-renderer artifact/flag to contributors and internal dogfood users.
  • Collect comparable startup, responsiveness, idle CPU, memory, crash-free-session, and feature-failure data, with privacy-preserving instrumentation.
  • Expand through canary/beta cohorts only when each release gate passes.
  • Keep the React renderer and a documented fallback path through the rollback window.
  • Migrate renderer-local persisted state with versioned, idempotent migrations; relay/Rust-owned data needs no renderer migration.
  • Freeze new React-only UI work during the final parity window or require a matching native implementation.
  • Obtain maintainer sign-off for functionality, accessibility, platform support, packaging, operations, and rollback readiness.
  • Make the native renderer the default, then remove obsolete React/Vite/Tauri renderer code and dependencies only after the agreed stability window.
  • Keep any intentional WebView islands and their minimum host dependencies; do not claim a browser-free binary while they remain.

Exit gate: the native renderer is the measured, supportable default; rollback has not been needed during the stability window; obsolete renderer code can be removed in a separate, reviewable change.

Proposed engineering rules and release gates

  • Each phase lands as small, reviewable PRs; no long-lived rewrite branch.
  • No phase deletes the current working renderer before a tested fallback exists.
  • Protocol/ABI compatibility, ownership, cancellation, and shutdown behavior are tests, not conventions.
  • Performance comparisons use matching datasets, user-visible boundaries, release builds, repeated samples, and disclosed host/process scope.
  • At minimum, scroll, prepend, composer, and live-event p90 must remain below one 60 Hz frame on supported hardware; final budgets should be set from project-owned test machines.
  • The virtual timeline must keep realized rows/widgets bounded as history grows.
  • Feature parity includes errors, offline/reconnect, keyboard, IME, accessibility, reduced motion, HiDPI, and platform packaging—not only happy-path screenshots.
  • Any retained WebView is explicit in the architecture and included in memory/startup/security measurements.
  • Native SDK stays behind a thin adapter so pre-1.0 API changes do not spread through Buzz feature code.

Risks and mitigations

Risk Mitigation
Native SDK is pre-1.0 and APIs may move Pin revisions; isolate it behind a renderer/platform adapter; upgrade deliberately with contract and screenshot tests.
Buzz's desktop feature surface makes a big-bang rewrite likely to stall Parallel target, vertical slices, route-level parity matrix, explicit exit gates, and a working fallback.
Rust/Zig FFI introduces ownership or threading bugs Narrow versioned C ABI, opaque handles, single ownership rules, bounded queues, generated/validated layouts, sanitizers, fuzz/round-trip tests.
Browser-specific features do not yet have native parity Retain small WebView islands with typed protocols rather than blocking the whole migration or embedding the entire app.
Renderer-proxy benchmark ratios are over-interpreted Treat Phase 0 as feasibility only; run real hosts and release-like, end-to-end measurements before rollout decisions.
Cross-platform behavior diverges Make Linux/macOS/Windows artifacts and platform tests a gate in every phase, not a final porting step.
Two renderers increase short-term maintenance Time-box the parallel period, freeze React-only additions near cutover, and remove old modules incrementally after parity.

Alternatives considered

  1. Continue optimizing React/Tauri only. This is the lowest-risk short-term path and worthwhile regardless, but it retains the browser renderer, its process/runtime overhead, and its platform-WebView failure modes.
  2. Use Native SDK with a TypeScript core. This is a valid native-runtime architecture and likely offers easier initial porting. Zig is proposed because the renderer is performance-sensitive and already needs an explicit native boundary to Rust; a Phase-1 implementation should still validate that the Zig productivity/maintenance tradeoff is acceptable.
  3. Move the UI into Rust. Rust-native UI frameworks would avoid an FFI boundary, but would be a different migration with different rendering, automation, platform, and ecosystem tradeoffs. The feasibility work here specifically validates Native SDK + Zig while preserving the existing Rust backend.
  4. Big-bang replacement. Rejected in favor of a parallel renderer, measurable vertical slices, and reversible rollout.

Offer to implement

I am happy to work on this migration and start with the remaining real-host Phase-0 smoke plus the Phase-1 bridge/native target. I can keep the work split into reviewable PRs, publish benchmark and parity updates at each gate, and adjust the architecture with maintainer feedback before any production cutover.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions