electron/README.md
This directory contains the Electron desktop application wrapper for OmniRoute.
electron/
├── main.js # Main process — window, tray, server lifecycle, CSP, IPC
├── preload.js # Preload script — secure IPC bridge with disposer pattern
├── package.json # Electron-specific dependencies & electron-builder config
├── types.d.ts # TypeScript definitions (AppInfo, ServerStatus, ElectronAPI)
└── assets/ # Application icons and resources
src/shared/hooks/
└── useElectron.ts # React hooks — useSyncExternalStore, zero re-renders
| Decision | Rationale |
|---|---|
waitForServer() polling | Prevents blank screen on cold start — polls http://localhost:PORT before loading |
stdio: 'pipe' | Captures server stdout/stderr for logging + readiness detection (not inherit) |
| Disposer pattern | onServerStatus() returns () => void for precise listener cleanup (no removeAllListeners) |
useSyncExternalStore | Zero re-renders for useIsElectron() — no useState + useEffect cycle |
| CSP via session headers | Content-Security-Policy restricts script-src, connect-src etc. per Electron best practices |
| Platform-conditional titlebar | titleBarStyle: 'hiddenInset' only on macOS; default on Windows/Linux |
npm run build
cd electron
npm install
npm run dev
cd electron
npm run dev
npm run build
cd electron
npm start
cd electron
npm run build
# Windows
npm run build:win
# macOS (x64 + arm64)
npm run build:mac
# Linux
npm run build:linux
Built applications are placed in dist-electron/:
.exe installer (NSIS) + portable .exe.dmg installer (Intel + Apple Silicon).AppImage.dmg from the Releases page..dmg file.OmniRoute.app to the Applications folder.⚠️ Note: The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:
bashxattr -cr /Applications/OmniRoute.appOr right-click the app → Open → Open (to bypass Gatekeeper on first launch).
Installer (Recommended):
OmniRoute.Setup.*.exe from Releases.Portable (No Installation):
OmniRoute.exe from Releases..AppImage from Releases.chmod +x OmniRoute-*.AppImage
./OmniRoute-*.AppImage
By default the desktop shell spawns and manages its own bundled Next.js server. If you already run OmniRoute elsewhere — most commonly in a Docker/OrbStack container, so provider credentials and env-var handling stay isolated from the host — you can point the shell at that instance instead, so it's purely a native window + tray onto a server you already run.
Via the tray menu: Remote Server → Connect to Remote Server…, enter the server's
URL (e.g. http://localhost:20128), and save. Leave the field blank and save to
disconnect and go back to the local embedded server. The preference persists across
restarts in <data dir>/electron-preferences.json (see DATA_DIR above for where that
lives on your platform).
Via environment variable: set OMNIROUTE_REMOTE_URL before launching the app (e.g.
OMNIROUTE_REMOTE_URL=http://localhost:20128 npm run dev, or export it in the
environment that launches the packaged app). The env var always wins over the persisted
preference and is session-scoped — it doesn't get written to the prefs file.
Only http:// and https:// URLs are accepted; anything else is rejected before the
window loads.
| Variable | Default | Description |
|---|---|---|
OMNIROUTE_PORT | 20128 | Server port |
OMNIROUTE_MEMORY_MB | 512 | Node.js heap limit (64–16384 MB) |
OMNIROUTE_REMOTE_URL | (unset) | Attach to this server instead of spawning a local one — see Remote Server Mode |
NODE_ENV | production | Set to development for dev mode |
Place your icons in assets/:
icon.ico — Windows icon (256×256)icon.icns — macOS icon bundleicon.png — Linux/general use (512×512)tray-icon.png — System tray icon (16×16 or 32×32)| Channel | Returns | Description |
|---|---|---|
get-app-info | AppInfo | App name, version, platform, isDev, port, remoteServerUrl |
open-external | void | Open URL in default browser (http/https only) |
get-data-dir | string | Get userData directory path |
restart-server | { success } | Stop + restart server (5s timeout + SIGKILL) |
| Channel | Description |
|---|---|
window-minimize | Minimize window |
window-maximize | Toggle maximize/restore |
window-close | Close window (minimize to tray) |
| Channel | Payload | Emitted When |
|---|---|---|
server-status | ServerStatus | Server starts, stops, errors, or restarts |
port-changed | number | Port change via tray menu |
Note: Listeners return disposer functions for precise cleanup. See
useServerStatusandusePortChangedhooks.
| Feature | Implementation |
|---|---|
| Context Isolation | contextIsolation: true — renderer cannot access Node.js |
| Node Integration | nodeIntegration: false — no require() in renderer |
| IPC Whitelist | Channel names validated in preload via safeInvoke/safeSend/safeOn |
| URL Validation | shell.openExternal() only allows http: / https: protocols |
| CSP | Content-Security-Policy header set via session.webRequest.onHeadersReceived |
| Web Security | webSecurity: true — same-origin policy enforced |
| Hook | Returns | Description |
|---|---|---|
useIsElectron() | boolean | Zero-render detection via useSyncExternalStore |
useElectronAppInfo() | { appInfo, loading, error } | App info from main process |
useDataDir() | { dataDir, loading, error } | User data directory |
useWindowControls() | { minimize, maximize, close } | Window control actions |
useOpenExternal() | { openExternal } | Open URLs in browser |
useServerControls() | { restart, restarting } | Server restart control |
useServerStatus(cb) | Disposer | Listen for server status events |
usePortChanged(cb) | Disposer | Listen for port change events |
lsof -i :20128[Electron] prefix.build/next/standalone[Server] and [Server:err] log outputEnsure you have build tools installed:
build-essential, libsecret-1-devMIT