Electron Desktop Guide
Source of truth:
electron/workspace Last updated: 2026-06-28 — v3.8.40
OmniRoute ships a cross-platform desktop app (Windows / macOS / Linux) built on
Electron 41 + electron-builder 26.10. The desktop app spawns the Next.js
standalone server as a child process, points a BrowserWindow at it, and adds a
system tray, auto-updater, IPC bridge, and zero-config secret bootstrap.
Architecture
Section titled “Architecture”┌──────────────────────────────────────────────┐│ Electron main process (electron/main.js) ││ ├─ Single-instance lock ││ ├─ Child process: Next.js standalone server ││ │ (spawned with Electron's Node runtime) ││ ├─ BrowserWindow → http://localhost:PORT ││ ├─ System tray + context menu ││ ├─ Auto-update via electron-updater ││ ├─ Content Security Policy (session headers) ││ └─ Secret bootstrap (JWT / API_KEY_SECRET) │└──────────────────────────────────────────────┘ ↕ IPC bridge (electron/preload.js)┌──────────────────────────────────────────────┐│ Renderer (Next.js dashboard) ││ window.electronAPI.* (contextIsolation) │└──────────────────────────────────────────────┘Versions
Section titled “Versions”Confirmed from electron/package.json:
| Package | Version |
|---|---|
electron |
^43.4.1 |
electron-builder |
^26.15.3 |
electron-updater |
^6.8.9 |
better-sqlite3 |
root ^13.0.2 (Node-API prebuilds — no Electron rebuild) |
| App version | 3.8.0 |
| App id | online.omniroute.desktop |
| Product name | OmniRoute |
Scripts (root package.json)
Section titled “Scripts (root package.json)”| Script | Purpose |
|---|---|
npm run electron:dev |
Starts npm run dev + waits for localhost:20128 + launches Electron |
npm run electron:build |
Builds Next.js then runs electron-builder for the current OS |
npm run electron:build:win |
Builds Windows NSIS installer + portable (x64) |
npm run electron:build:mac |
Builds macOS DMG (Intel + Apple Silicon) |
npm run electron:build:linux |
Builds Linux AppImage + DEB (x64 + arm64) |
npm run electron:smoke:packaged |
Launches packaged binary and probes /login for HTTP 200, then shuts down |
The electron/ workspace also exposes:
npm run prepare:bundle— runsscripts/build/prepare-electron-standalone.mjsnpm run build:mac-x64/build:mac-arm64— single-arch macOS buildsnpm run pack— directory-only build for local testing (no installer)
Directory Layout
Section titled “Directory Layout”electron/├── package.json # Electron deps + electron-builder config├── main.js # Main process (24 KB — see annotations below)├── preload.js # contextBridge IPC bridge├── types.d.ts # AppInfo / ServerStatus / ElectronAPI types├── README.md # In-workspace notes├── assets/ # icon.png, icon.ico, icon.icns, tray-icon.png└── dist-electron/ # electron-builder output (gitignored)
scripts/├── build/│ └── prepare-electron-standalone.mjs # Stages .next/electron-standalone bundle└── dev/ └── smoke-electron-packaged.mjs # Post-build smoke testBoth main.js and preload.js are CommonJS .js files, not TypeScript. The
renderer-side typings live in electron/types.d.ts.
IPC Bridge (preload.js)
Section titled “IPC Bridge (preload.js)”The preload exposes a whitelisted API on window.electronAPI using contextBridge
with contextIsolation: true and nodeIntegration: false.
const VALID_CHANNELS = { invoke: [ "get-app-info", "open-external", "get-data-dir", "restart-server", "check-for-updates", "download-update", "install-update", "get-app-version", ], send: ["window-minimize", "window-maximize", "window-close"], receive: ["server-status", "port-changed", "update-status"],};Exposed methods:
| Renderer call | Type |
|---|---|
getAppInfo() → { name, version, platform, isDev, port } |
invoke |
openExternal(url) |
invoke |
getDataDir() |
invoke |
restartServer() |
invoke |
getAppVersion() |
invoke |
checkForUpdates() / downloadUpdate() / installUpdate() |
invoke |
minimizeWindow() / maximizeWindow() / closeWindow() |
send |
onServerStatus(cb) / onPortChanged(cb) / onUpdateStatus(cb) |
receive (returns disposer) |
The receive helpers return a disposer function rather than relying on
removeAllListeners — this prevents listener accumulation when React components
remount.
Server Lifecycle
Section titled “Server Lifecycle”main.js spawns the Next.js standalone bundle directly with the Electron Node
runtime to avoid native-module ABI mismatch with system Node:
spawn(process.execPath, [serverScript], { cwd: NEXT_SERVER_PATH, env: { ...serverEnv, PORT, NODE_ENV: "production", ELECTRON_RUN_AS_NODE: "1", NODE_PATH }, stdio: "pipe",});Highlights:
waitForServer()polls the URL up to 30 s before showing the window (no blank screen on cold start).stdio: "pipe"captures stdout/stderr; ready phrases (Ready/listening) emitserver-status: runningover IPC.before-quitwaits up to 5 s for graceful SIGTERM (WAL checkpoint) then sends SIGKILL.- Port switcher in the tray (
20128,3000,8080) stops and restarts the server, then reloads the BrowserWindow.
Zero-config Secret Bootstrap
Section titled “Zero-config Secret Bootstrap”On first launch, the main process auto-generates and persists missing secrets:
| Secret | Source |
|---|---|
JWT_SECRET |
crypto.randomBytes(64).toString("hex") |
STORAGE_ENCRYPTION_KEY |
crypto.randomBytes(32).toString("hex") (refuses if encrypted creds already exist) |
API_KEY_SECRET |
crypto.randomBytes(32).toString("hex") |
Persisted to <DATA_DIR>/server.env. DATA_DIR resolves to:
- Windows:
%APPDATA%\omniroute - Linux:
$XDG_CONFIG_HOME/omnirouteor~/.omniroute - macOS:
~/.omniroute
Window & Tray
Section titled “Window & Tray”BrowserWindow: 1400×900 (min 1024×700),backgroundColor: "#0a0a0a".- macOS:
titleBarStyle: "hiddenInset", traffic-light at{ x: 16, y: 16 }. - Windows/Linux: native title bar.
- Close button minimizes to tray; the tray menu has Open OmniRoute, Open Dashboard (external browser), Server Port submenu, Check for Updates, Quit.
Content Security Policy
Section titled “Content Security Policy”Set via session.defaultSession.webRequest.onHeadersReceived. Notable directives:
frame-ancestors 'none',object-src 'none',child-src 'none'connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://*.omniroute.online https://*.omniroute.dev- Dev mode adds
'unsafe-eval'toscript-srconly
Auto-update
Section titled “Auto-update”Uses electron-updater with the GitHub provider (diegosouzapw/OmniRoute).
autoDownload = false,autoInstallOnAppQuit = true- Events forwarded to renderer via
update-statusIPC:checking,available,not-available,downloading(withpercent),downloaded,error installUpdate()kills the server then callsautoUpdater.quitAndInstall()- Skipped in dev mode (
!app.isPackaged)
Build Pipeline
Section titled “Build Pipeline”npm run build→ Next.js standalone in.next/standalone.prepare-electron-standalone.mjs→ re-stages into.next/electron-standaloneand rewrites absolute paths insideserver.js+required-server-files.jsonso the bundle is relocatable.electron-builderpackagesmain.js,preload.js,node_modules, andextraResources: { ../.next/electron-standalone → app }.
Build targets
Section titled “Build targets”| OS | Targets |
|---|---|
| Windows | NSIS installer + portable (x64) |
| macOS | DMG (Intel + arm64, drag-to-Applications) |
| Linux | AppImage + DEB (x64 + arm64) |
NSIS settings: oneClick: false, lets the user choose the install directory, creates Desktop and Start-Menu shortcuts.
Smoke Testing Packaged Build
Section titled “Smoke Testing Packaged Build”npm run electron:smoke:packagedscripts/dev/smoke-electron-packaged.mjs:
- Auto-discovers the packaged binary in
electron/dist-electron/for the current platform. - Launches with isolated
HOME/APPDATA/XDG_*directories so it doesn’t touch developer data. - Polls
http://127.0.0.1:20128/loginfor HTTP 200 within 45 s. - Watches stderr/stdout for fatal patterns (
Cannot find module,MODULE_NOT_FOUND,ERR_DLOPEN_FAILED,Failed to start server, etc.). - Waits 2 s of stable runtime after readiness, then issues SIGTERM and waits for the port to free.
- In CI, automatically passes
--no-sandbox --disable-gpu(and--disable-dev-shm-usageon Linux).
Env overrides: ELECTRON_SMOKE_APP_EXECUTABLE, ELECTRON_SMOKE_URL, ELECTRON_SMOKE_TIMEOUT_MS, ELECTRON_SMOKE_SETTLE_MS, ELECTRON_SMOKE_DATA_DIR, ELECTRON_SMOKE_KEEP_DATA, ELECTRON_SMOKE_STREAM_LOGS.
Code Signing
Section titled “Code Signing”electron/package.json does not wire signing credentials directly. Pass them via env vars to electron-builder:
export APPLE_ID=<email>export APPLE_APP_SPECIFIC_PASSWORD=<password>export APPLE_TEAM_ID=<id>export CSC_LINK=path/to/cert.p12export CSC_KEY_PASSWORD=<cert-password>npm run electron:build:macWindows
Section titled “Windows”export CSC_LINK=path/to/cert.pfxexport CSC_KEY_PASSWORD=<cert-password>npm run electron:build:winAppImage signing is optional — set LINUX_GPG_KEY if signing.
Distribution
Section titled “Distribution”Artifacts land in electron/dist-electron/:
OmniRoute.Setup.X.Y.Z.exe,OmniRoute X.Y.Z.exe(Windows)OmniRoute-X.Y.Z-mac.dmg,OmniRoute-X.Y.Z-arm64-mac.dmg(macOS)OmniRoute-X.Y.Z.AppImage,omniroute-desktop_X.Y.Z_amd64.deb(Linux)
Releases are published to GitHub Releases (diegosouzapw/OmniRoute), which is also where electron-updater checks for new versions.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Fix |
|---|---|
Cannot find module 'better-sqlite3' after Electron major bump |
better-sqlite3 v13 ships Node-API prebuilds — re-run npm install at the root and prepare:bundle (it verifies the prebuild for the current platform) |
ERR_DLOPEN_FAILED for native module |
Re-run prepare:bundle — it fails fast when the Node-API prebuild for the current platform is missing |
| Window appears blank on Linux | Confirm Next.js server actually bound to PORT (check [Server] logs) |
| macOS notarization stalls | Ensure APPLE_* vars are exported, not just in .env |
| Windows SmartScreen warning | Sign with EV cert, or users right-click → “Run anyway” |
| Smoke test fails with port-in-use | Stop any local dev server on 20128 before running electron:smoke:packaged |
See Also
Section titled “See Also”- SETUP_GUIDE.md
- RELEASE_CHECKLIST.md
- Source:
electron/main.js,electron/preload.js,electron/package.json - Helpers:
scripts/build/prepare-electron-standalone.mjs,scripts/dev/smoke-electron-packaged.mjs
HagiCode
HagiCode is an agentic coding workspace: structured workflows, multi-agent execution, and Hero Dungeon views turn ideas into shipped software.
Turn ideas into polished, usable software with a smarter, faster, and more enjoyable agentic coding workflow.

- SmartStructured workflows turn intent into an executable path from idea to shipped change.
- EfficientMulti-agent workflows keep research, implementation, and review moving in parallel.
- FunHero Dungeon interfaces make long coding sessions visual, collaborative, and rewarding.