Release v3.8.1 — feature flags settings page, bracketed combo names, security hardening, multi-driver SQLite
13 KiB
| title | version | lastUpdated |
|---|---|---|
| Electron Desktop Guide | 3.8.1 | 2026-05-13 |
Electron Desktop Guide
Source of truth:
electron/workspace Last updated: 2026-05-13 — v3.8.0
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
┌──────────────────────────────────────────────┐
│ 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
Confirmed from electron/package.json:
| Package | Version |
|---|---|
electron |
^41.5.1 |
electron-builder |
^26.10.0 |
electron-updater |
^6.8.5 |
better-sqlite3 |
^12.9.0 |
| App version | 3.8.0 |
| App id | online.omniroute.desktop |
| Product name | OmniRoute |
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
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 test
Both 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)
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
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
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
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
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
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
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
| 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
npm run electron:smoke:packaged
scripts/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
electron/package.json does not wire signing credentials directly. Pass them via env vars to electron-builder:
macOS
export APPLE_ID=<email>
export APPLE_APP_SPECIFIC_PASSWORD=<password>
export APPLE_TEAM_ID=<id>
export CSC_LINK=path/to/cert.p12
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:mac
Windows
export CSC_LINK=path/to/cert.pfx
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:win
Linux
AppImage signing is optional — set LINUX_GPG_KEY if signing.
Distribution
Artifacts land in electron/dist-electron/:
OmniRoute Setup X.Y.Z.exe,OmniRoute-X.Y.Z-portable.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
| Symptom | Fix |
|---|---|
Cannot find module 'better-sqlite3' after Electron major bump |
cd electron && npm rebuild |
ERR_DLOPEN_FAILED for native module |
Re-run prepare:bundle and verify ABI matches Electron's Node |
| 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
- 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