# 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<T>(command: Command) => Promise<T>` → channel `cqrs:command`.
- `query<T>(query: Query) => Promise<T>` → 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 |
