Every .md under docs/{architecture,guides,reference,frameworks,routing,
security,compression,ops,diagrams} plus docs/README.md now opens with:
---
title: "<inferred from first H1>"
version: 3.8.0
lastUpdated: 2026-05-13
---
46 files updated (no docs were skipped — none had pre-existing
frontmatter). [slug]/page.tsx already reads frontmatter.version and
frontmatter.lastUpdated via gray-matter and renders a "v3.8.0" pill
plus a "Last updated" caption, so the UI picks these up automatically.
Helper: scripts/docs/add-frontmatter.mjs — idempotent (skips files that
already start with `---`), falls back to a humanized basename when no
leading H1 exists. Excludes docs/i18n/, docs/screenshots/,
docs/superpowers/, docs/diagrams/exported/. Re-runnable safely.
Also regenerated src/app/docs/lib/docs-auto-generated.ts: 44 docs across
8 sections (Architecture / Guides / Reference / Frameworks / Routing /
Security / Compression / Ops), which now includes the 14 docs that were
missing from the v3.7 sidebar (Cloud Agents, Guardrails, Memory, Skills,
Webhooks, Evals, Authz, Agent Protocols, Repository Map, Provider
Reference, Reasoning Replay, Stealth Guide, Tunnels Guide, Electron
Guide).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Electron Desktop Guide | 3.8.0 | 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