# Electron IPC Bridge > **Area:** ipc-bridge > **Status:** Active > **Last updated:** 2026-05-25 ## Overview The Electron IPC bridge is the only path through which the Angular renderer can reach the desktop runtime — the filesystem, the local SQLite database, OS APIs, the update flow, plugin loading, and the in-process Local API server. The renderer cannot import `electron`, `node:fs`, TypeORM, or any other privileged module directly; every privileged operation crosses the preload `contextBridge` boundary as a typed IPC call. This area documents the surface itself: how it is registered, how it is consumed, and what guarantees do (or do not) hold. ## Responsibilities - Expose a frozen, allow-listed set of methods on the renderer's global window object via the preload bridge. - Register one `ipcMain` handler per exposed method, grouped by concern (`system`, `window-controls`, `cqrs`). - Provide a CQRS abstraction over the local database (commands + queries dispatched through two generic channels). - Translate main-process operations into renderer-safe values (file paths → URLs, native errors → structured responses where appropriate). This area does **not** own: - The schema or business logic behind any specific command/query (those live in `electron/cqrs/handlers/` and the affected product-client domains). - The plugin manifest contract (see [plugin-system](./plugin-system.md)) — only the IPC methods that surface it. - WebSocket signaling (see [websocket-envelopes](./websocket-envelopes.md)) — that bypasses Electron entirely. ## Key concepts - **Preload bridge** — `electron/preload.ts`. The sole place `contextBridge.exposeInMainWorld` runs. Adding a method here requires a matching `ipcMain.handle` / `ipcMain.on` on the main side. - **Window surface** — exposed as `window.electronAPI` on the renderer. (Note: `electron/CONTEXT.md` refers to this as `window.api.*` — the documented intent. The literal global today is `electronAPI`. TODO: pick one and align.) - **CQRS channel** — two reserved channels `cqrs:command` and `cqrs:query` route every typed `Command`/`Query` through a single dispatch step, instead of one channel per operation. - **Handler setup function** — registered once at app boot from `electron/ipc/index.ts`: `setupCqrsHandlers`, `setupSystemHandlers`, `setupWindowControlHandlers`. - **Renderer bridge service** — `ElectronBridgeService` in `toju-app/src/app/core/platform/electron/electron-bridge.service.ts` is the Angular-side wrapper; domain services inject it rather than reaching for `window.electronAPI` directly. --- ## Surface catalogue Defined in `electron/preload.ts`. Approximately 50 methods, grouped below by concern. For exact signatures see the file. ### Window controls (fire-and-forget) - `minimizeWindow`, `maximizeWindow`, `closeWindow` — channels `window-minimize`, `window-maximize`, `window-close`. Implementation: `electron/ipc/window-controls.ts`. Uses `ipcMain.on` (no return value). ### Screen share & media - `getSources` — DesktopCapturer source enumeration. - Linux audio routing for screen-share monitor capture: `prepareLinuxScreenShareAudioRouting`, `activateLinuxScreenShareAudioRouting`, `deactivateLinuxScreenShareAudioRouting`, `startLinuxScreenShareMonitorCapture`, `stopLinuxScreenShareMonitorCapture`. - Event listeners: `onLinuxScreenShareMonitorAudioChunk`, `onLinuxScreenShareMonitorAudioEnded`. ### Process & game detection - `getRunningProcessNames` (via `electron/process-list.ts`). - `getActiveGameCandidate` (via `electron/game-detection/`). - `getIgnoredGameProcesses`, `setIgnoredGameProcesses`. ### File system - `readFile`, `readFileChunk`, `getFileSize`, `writeFile`, `appendFile`, `deleteFile`, `fileExists`, `getFileUrl`, `ensureDir`, `saveFileAs`, `saveExistingFileAs`, `openFilePath`, `readClipboardFiles`. - `getFileUrl` is the canonical way for the renderer to display a local file via `file://` — direct path access is forbidden. ### Theme & plugins (filesystem-backed) - `getSavedThemesPath`, `listSavedThemes`, `readSavedTheme`, `writeSavedTheme`, `deleteSavedTheme`. - `getLocalPluginsPath`, `listLocalPluginManifests`. See [plugin-system](./plugin-system.md) for the manifest contract. ### Settings & notifications - `getDesktopSettings`, `setDesktopSettings`. - `showDesktopNotification`, `requestWindowAttention`, `clearWindowAttention`, `onWindowStateChanged`. ### Auto-update - `getAutoUpdateState`, `getAutoUpdateServerHealth`, `configureAutoUpdateContext`, `checkForAppUpdates`, `restartToApplyUpdate`, `onAutoUpdateStateChanged`. ### Local API & docs - `getLocalApiStatus`, `openLocalApiDocs`, `openDocusaurusDocs`. The Local API server hosts the prebuilt Docusaurus bundle inside the desktop app — see `electron/api/local-api-server.ts`. ### App & deep links - `relaunchApp`, `consumePendingDeepLink`, `onDeepLinkReceived`, `getAppDataPath`, `openCurrentDataFolder`. ### Data management - `exportUserData`, `importUserData`, `eraseUserData`. Backed by `electron/data-archive.ts`. ### Clipboard & context menu - `copyImageToClipboard`, `onContextMenu`, `contextMenuCommand`. ### Idle state - `getIdleState`, `onIdleStateChanged`. Backed by `electron/idle/`. ### CQRS (typed database access) - `command(command: Command) => Promise` → channel `cqrs:command`. - `query(query: Query) => Promise` → channel `cqrs:query`. - Command and query union types live in `electron/cqrs/types.ts`. Handlers are built dynamically per `DataSource` via `buildCommandHandlers(dataSource)` and `buildQueryHandlers(dataSource)` in `electron/ipc/cqrs.ts`. - Current commands: `SaveMessage`, `DeleteMessage`, `UpdateMessage`, `ClearRoomMessages`, `SaveReaction`, `RemoveReaction`, `SaveUser`, `SetCurrentUserId`, `UpdateUser`, `SaveRoom`, `DeleteRoom`, `UpdateRoom`, `SaveBan`, `RemoveBan`, `SaveAttachment`, `DeleteAttachmentsForMessage`, `SavePluginData`, `DeletePluginData`, `SaveMeta`, `ClearAllData`. - Current queries: `GetMessages`, `GetMessagesSince`, `GetMessageById`, `GetReactionsForMessage`, `GetUser`, `GetCurrentUser`, `GetCurrentUserId`, `GetUsersByRoom`, `GetRoom`, `GetAllRooms`, `GetBansForRoom`, `IsUserBanned`, `GetAttachmentsForMessage`, `GetAllAttachments`, `GetPluginData`, `GetMeta`. - Unknown `type` raises `Error("No command/query handler for type: ${type}")`. --- ## Renderer consumption - **`ElectronBridgeService`** (`toju-app/src/app/core/platform/electron/electron-bridge.service.ts`) — provides `getApi(): ElectronApi | null` and `requireApi(): ElectronApi`. Domain services inject the bridge service, never `window` directly. This also makes the bridge mockable for spec runs and the website preview (where `window.electronAPI` is absent). - **CQRS wrapper**: `toju-app/src/app/infrastructure/persistence/electron-database.service.ts` wraps `api.command()` / `api.query()` with typed helpers; product-client domains use this rather than calling CQRS directly. - **Per-domain consumers**: file I/O (`attachment`), theme (`theme`), profile-avatar, notifications, idle (used by presence), and game-activity domains each inject the bridge to reach their respective IPC slice. --- ## Error handling `electron/CONTEXT.md` says: > IPC handler errors are translated to typed error envelopes before crossing back into the renderer — the renderer never sees a raw `Error` from main. In practice today: - The CQRS layer throws raw `Error` objects on unknown `type` (caller sees the serialized message). - Most `electron/ipc/system.ts` handlers catch errors and return structured response objects (e.g. `{ opened: false, reason: string }`), but the shape is per-handler, not centralised. - There is no global error-envelope wrapper around `ipcMain.handle`. **TODO**: reconcile the CONTEXT.md invariant with reality — either introduce a shared error-envelope wrapper or update the invariant to match the per-handler convention. Until then, treat error shapes as a per-method concern. --- ## Technical implementation - **Preload**: `electron/preload.ts` (single source of truth for the exposed surface). - **Registration**: `electron/ipc/index.ts` calls three setup functions at app boot — `setupCqrsHandlers`, `setupSystemHandlers`, `setupWindowControlHandlers`. - **System handlers**: `electron/ipc/system.ts` (~40 channels, ~780 lines). - **Window controls**: `electron/ipc/window-controls.ts` (3 channels, fire-and-forget). - **CQRS handlers**: `electron/ipc/cqrs.ts` plus typed command/query unions in `electron/cqrs/types.ts` and per-handler implementations under `electron/cqrs/handlers/`. - **Local SQLite access** is gated behind CQRS — no other channel exposes the database directly. See `electron/data-source.ts` and `electron/entities/`. ## Invariants - The renderer never imports `electron`, Node APIs, or TypeORM directly. (Enforced by Electron's `contextIsolation` + no `nodeIntegration`.) - Every method on `window.electronAPI` has exactly one IPC channel and exactly one main-process handler. - Schema mutations go through a TypeORM migration in `electron/migrations/`; raw SQL never crosses the IPC bridge. - All file access is path-based on the main side, URL-based on the renderer side (`getFileUrl`). ## Testing - `electron/plugin-library.spec.ts` — plugin discovery (touches the same IPC path but tests the library, not the channel). - `electron/idle/idle-monitor.spec.ts` — idle source unit test. - **TODO**: no spec covers `preload.ts` exposure, the system handler set, the CQRS dispatcher, or the error path. Renderer-side `ElectronBridgeService` spec status not verified. ## Security considerations - `contextIsolation: true` + no `nodeIntegration` in the renderer; `electron/preload.ts` is the only crossing. - Adding a channel requires touching both `preload.ts` and `electron/ipc/`. There is no dynamic channel registration. - File-system handlers should validate paths against user-data scope — TODO: audit `system.ts` for path-traversal protections beyond what the plugin loader does. - Deep-link handling: `consumePendingDeepLink` returns a queued URL; validation lives in renderer routing. TODO: confirm allow-list / scheme filtering on the main side. ## Performance considerations - IPC traffic is per-call serialization; large payloads (file chunks, attachment imports) go via `readFileChunk` + offsets instead of single `readFile` to avoid blocking the main process. - CQRS calls hit the local SQLite database synchronously inside the main process. There is no batching layer. ## Known issues and limitations - **Documented vs. actual API name** — the `window` global is `electronAPI`, not `api`. CONTEXT.md uses `window.api.*`. Reconcile in a future cleanup. - **No typed error envelope** despite the CONTEXT.md invariant. - **No preload-surface test** — additions are caught only at runtime / lint. ## Related features - **[plugin-system](./plugin-system.md)** — surfaces `getLocalPluginsPath`, `listLocalPluginManifests`, and plugin data CQRS commands through this bridge. - **[websocket-envelopes](./websocket-envelopes.md)** — the realtime path that bypasses the bridge; included here only to delineate the two surfaces. - **[voice-signaling](./voice-signaling.md)** — uses `getSources` and the Linux audio routing methods for screen-share media capture. ## Changelog | Date | Change | |------|--------| | 2026-05-25 | Initial documentation |