Bundle a vanilla Node runtime for the packaged Electron backend

Date: 2026-05-22 Status: Approved (design); plan pending Targets: macOS arm64, macOS x64, Windows x64, Linux x64

Problem

The server-side GPU compositor (packages/gpu, via the webgpu/dawn.node package) reads pixels back from the GPU with getMappedRange(). Under Electron’s embedded Node — which runs with the compile-time V8 heap sandbox (NODE_MODULE_VERSION 140) — that call throws External buffers are not allowed, because Dawn hands back an external (off-heap) ArrayBuffer the sandbox refuses to wrap. No runtime flag disables the V8 sandbox, and ELECTRON_RUN_AS_NODE is still sandboxed.

Dev already side-steps this: the backend is spawned as a separate, non-sandboxed system-Node child process (electron/src/server.ts dev branch → Watchdog command path). Prod still uses utilityProcess.fork (Electron’s sandboxed embedded Node), so GPU compositing cannot work in the packaged app. Separately, the webgpu package is not bundled at all today, so import("webgpu") already fails in a packaged build.

Goal

Make the packaged app run the backend on a bundled, non-sandboxed vanilla Node, so the GPU compositor (and any future server-side GPU work) behaves identically in dev and prod. Ship for all four targets.

Non-goals

Verified facts (current state)

Design

1. Runtime unification (server.ts)

Prod backend launches like dev — a non-sandboxed Node child process via the Watchdog command path. Prod branch changes from:

{ modulePath: backendEntryPoint, ... }                       // utilityProcess.fork

to:

{ command: bundledNodePath, args: [backendEntryPoint], ... } // child_process.spawn

where

No Watchdog change.

2. Node runtime acquisition — electron/scripts/fetch-node-runtime.mjs

3. Bundling — bundle-backend.mjs

4. Packaging — after-pack.cjs (per-arch)

5. Native-module ABI — rebuild-native.mjs (postinstall)

6. Signing / notarization

7. Verification / tests

Risks / open items