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. Rich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate, creating an engaging development experience.
Explore an engaging development experience through rich, convenient UI interactions.
- SmartOpenSpec workflows turn intent into an executable path, from proposal to design, tasks, and a shipped change.
- EfficientMulti-agent execution keeps research, implementation, and review moving in parallel, across several repositories and the Agent CLIs you already use.
- InterestingRich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate.
Download for Windows
Share HagiCode
Turn ideas into shipped software.
HagiCode is an agentic coding workspace. OpenSpec workflows plan the change, multi-agent execution carries it out, and Hero Scenario makes the long run visible, so ideas become shipped software.
- Smart
- OpenSpec workflows turn intent into a path from proposal to shipped change.
- Efficient
- Multi-agent execution runs research, build, and review in parallel.
- Fun
- Rich UI interactions make planning, agent work, and projects easier to follow.
© 2026 HagiCode | Powered By hagilight-starlight@0.21.0 | Branch: main | Commit: de60bcd | Built:

