Files
Toju/agents-docs/features/ipc-bridge.md
brogeby b19c39208c docs: populate initial cross-context feature docs
Add area-level documentation for the five most significant cross-context
feature areas under agents-docs/features/:

- websocket-envelopes: full envelope catalogue, lifecycle, dispatcher
- ipc-bridge: window.electronAPI surface, IPC channels, CQRS dispatch
- plugin-system: manifest contract, runtime, capabilities, plugin-support API
- server-directory: REST endpoints, CQRS, entities, business rules
- voice-signaling: mesh signaling, RNNoise pipeline, domain split

Update agents-docs/FEATURES.md index alphabetically and remove the
"no cross-context feature docs" placeholder.

Each doc records honest TODOs for verified gaps (stale signaling-contracts.ts,
window.api vs window.electronAPI mismatch, IPC error envelope drift from
CONTEXT.md, missing OpenAPI coverage for server-directory routes, no
envelope round-trip test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:36:36 +02:00

11 KiB

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) — only the IPC methods that surface it.
  • WebSocket signaling (see websocket-envelopes) — that bypasses Electron entirely.

Key concepts

  • Preload bridgeelectron/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 serviceElectronBridgeService 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 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.
  • 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.
  • plugin-system — surfaces getLocalPluginsPath, listLocalPluginManifests, and plugin data CQRS commands through this bridge.
  • websocket-envelopes — the realtime path that bypasses the bridge; included here only to delineate the two surfaces.
  • voice-signaling — uses getSources and the Linux audio routing methods for screen-share media capture.

Changelog

Date Change
2026-05-25 Initial documentation