Back to Omniroute

OmniRoute Electron Desktop App

electron/README.md

3.8.4910.6 KB
Original Source

OmniRoute Electron Desktop App

This directory contains the Electron desktop application wrapper for OmniRoute.

Architecture (v1.6.4)

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

Key Design Decisions

DecisionRationale
waitForServer() pollingPrevents blank screen on cold start — polls http://localhost:PORT before loading
stdio: 'pipe'Captures server stdout/stderr for logging + readiness detection (not inherit)
Disposer patternonServerStatus() returns () => void for precise listener cleanup (no removeAllListeners)
useSyncExternalStoreZero re-renders for useIsElectron() — no useState + useEffect cycle
CSP via session headersContent-Security-Policy restricts script-src, connect-src etc. per Electron best practices
Platform-conditional titlebartitleBarStyle: 'hiddenInset' only on macOS; default on Windows/Linux

Development

Prerequisites

  1. Build the Next.js app first:
bash
npm run build
  1. Install Electron dependencies:
bash
cd electron
npm install

Running in Development

  1. Start the Next.js development server:
bash
npm run dev
  1. In another terminal, start Electron:
bash
cd electron
npm run dev

Running in Production Mode

  1. Build Next.js in standalone mode:
bash
npm run build
  1. Start Electron:
bash
cd electron
npm start

Building

Build for Current Platform

bash
cd electron
npm run build

Build for Specific Platforms

bash
# Windows
npm run build:win

# macOS (x64 + arm64)
npm run build:mac

# Linux
npm run build:linux

Output

Built applications are placed in dist-electron/:

  • Windows: .exe installer (NSIS) + portable .exe
  • macOS: .dmg installer (Intel + Apple Silicon)
  • Linux: .AppImage

Installation

macOS

  1. Download the latest .dmg from the Releases page.
  2. Open the .dmg file.
  3. Drag OmniRoute.app to the Applications folder.
  4. Launch from Applications.

⚠️ Note: The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:

bash
xattr -cr /Applications/OmniRoute.app

Or right-click the app → Open → Open (to bypass Gatekeeper on first launch).

Windows

Installer (Recommended):

  1. Download OmniRoute.Setup.*.exe from Releases.
  2. Run the installer.
  3. Launch from Start Menu or Desktop shortcut.

Portable (No Installation):

  1. Download OmniRoute.exe from Releases.
  2. Run directly from any folder.

Linux

  1. Download the .AppImage from Releases.
  2. Make it executable:
    bash
    chmod +x OmniRoute-*.AppImage
    
  3. Run:
    bash
    ./OmniRoute-*.AppImage
    

Features

  • Server Readiness — Waits for health check before showing window
  • System Tray — Minimize to tray with quick actions (open, port change, quit)
  • Port Management — Change port from tray menu (server restarts automatically)
  • Remote Server Mode — Point the shell at an already-running OmniRoute server (e.g. a Docker/OrbStack container, or another machine) instead of spawning a local one — see below
  • Window Controls — Custom minimize, maximize, close via IPC
  • Content Security Policy — Restrictive CSP via session headers
  • Offline Support — Bundled Next.js standalone server
  • Single Instance — Only one app instance can run at a time

Remote Server Mode

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.

Configuration

Environment Variables

VariableDefaultDescription
OMNIROUTE_PORT20128Server port
OMNIROUTE_MEMORY_MB512Node.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_ENVproductionSet to development for dev mode

Custom Icon

Place your icons in assets/:

  • icon.ico — Windows icon (256×256)
  • icon.icns — macOS icon bundle
  • icon.png — Linux/general use (512×512)
  • tray-icon.png — System tray icon (16×16 or 32×32)

IPC Channels

Invoke (Renderer → Main, async)

ChannelReturnsDescription
get-app-infoAppInfoApp name, version, platform, isDev, port, remoteServerUrl
open-externalvoidOpen URL in default browser (http/https only)
get-data-dirstringGet userData directory path
restart-server{ success }Stop + restart server (5s timeout + SIGKILL)

Send (Renderer → Main, fire-and-forget)

ChannelDescription
window-minimizeMinimize window
window-maximizeToggle maximize/restore
window-closeClose window (minimize to tray)

Receive (Main → Renderer, events)

ChannelPayloadEmitted When
server-statusServerStatusServer starts, stops, errors, or restarts
port-changednumberPort change via tray menu

Note: Listeners return disposer functions for precise cleanup. See useServerStatus and usePortChanged hooks.

Security

FeatureImplementation
Context IsolationcontextIsolation: true — renderer cannot access Node.js
Node IntegrationnodeIntegration: false — no require() in renderer
IPC WhitelistChannel names validated in preload via safeInvoke/safeSend/safeOn
URL Validationshell.openExternal() only allows http: / https: protocols
CSPContent-Security-Policy header set via session.webRequest.onHeadersReceived
Web SecuritywebSecurity: true — same-origin policy enforced

React Hooks

HookReturnsDescription
useIsElectron()booleanZero-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)DisposerListen for server status events
usePortChanged(cb)DisposerListen for port change events

Troubleshooting

App Won't Start

  1. Check if port 20128 is available: lsof -i :20128
  2. Check console logs for [Electron] prefix
  3. Verify the build output exists in .build/next/standalone

White Screen

  1. Verify Next.js build exists — server readiness waits 30s max
  2. Check [Server] and [Server:err] log output
  3. Look for CSP violations in developer console

Build Fails

Ensure you have build tools installed:

  • Windows: Visual Studio Build Tools
  • macOS: Xcode Command Line Tools
  • Linux: build-essential, libsecret-1-dev

License

MIT