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>
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
ipcMainhandler 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 bridge —
electron/preload.ts. The sole placecontextBridge.exposeInMainWorldruns. Adding a method here requires a matchingipcMain.handle/ipcMain.onon the main side. - Window surface — exposed as
window.electronAPIon the renderer. (Note:electron/CONTEXT.mdrefers to this aswindow.api.*— the documented intent. The literal global today iselectronAPI. TODO: pick one and align.) - CQRS channel — two reserved channels
cqrs:commandandcqrs:queryroute every typedCommand/Querythrough 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 —
ElectronBridgeServiceintoju-app/src/app/core/platform/electron/electron-bridge.service.tsis the Angular-side wrapper; domain services inject it rather than reaching forwindow.electronAPIdirectly.
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— channelswindow-minimize,window-maximize,window-close. Implementation:electron/ipc/window-controls.ts. UsesipcMain.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(viaelectron/process-list.ts).getActiveGameCandidate(viaelectron/game-detection/).getIgnoredGameProcesses,setIgnoredGameProcesses.
File system
readFile,readFileChunk,getFileSize,writeFile,appendFile,deleteFile,fileExists,getFileUrl,ensureDir,saveFileAs,saveExistingFileAs,openFilePath,readClipboardFiles.getFileUrlis the canonical way for the renderer to display a local file viafile://— 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 — seeelectron/api/local-api-server.ts.
App & deep links
relaunchApp,consumePendingDeepLink,onDeepLinkReceived,getAppDataPath,openCurrentDataFolder.
Data management
exportUserData,importUserData,eraseUserData. Backed byelectron/data-archive.ts.
Clipboard & context menu
copyImageToClipboard,onContextMenu,contextMenuCommand.
Idle state
getIdleState,onIdleStateChanged. Backed byelectron/idle/.
CQRS (typed database access)
command<T>(command: Command) => Promise<T>→ channelcqrs:command.query<T>(query: Query) => Promise<T>→ channelcqrs:query.- Command and query union types live in
electron/cqrs/types.ts. Handlers are built dynamically perDataSourceviabuildCommandHandlers(dataSource)andbuildQueryHandlers(dataSource)inelectron/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
typeraisesError("No command/query handler for type: ${type}").
Renderer consumption
ElectronBridgeService(toju-app/src/app/core/platform/electron/electron-bridge.service.ts) — providesgetApi(): ElectronApi | nullandrequireApi(): ElectronApi. Domain services inject the bridge service, neverwindowdirectly. This also makes the bridge mockable for spec runs and the website preview (wherewindow.electronAPIis absent).- CQRS wrapper:
toju-app/src/app/infrastructure/persistence/electron-database.service.tswrapsapi.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
Errorfrom main.
In practice today:
- The CQRS layer throws raw
Errorobjects on unknowntype(caller sees the serialized message). - Most
electron/ipc/system.tshandlers 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.tscalls 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.tsplus typed command/query unions inelectron/cqrs/types.tsand per-handler implementations underelectron/cqrs/handlers/. - Local SQLite access is gated behind CQRS — no other channel exposes the database directly. See
electron/data-source.tsandelectron/entities/.
Invariants
- The renderer never imports
electron, Node APIs, or TypeORM directly. (Enforced by Electron'scontextIsolation+ nonodeIntegration.) - Every method on
window.electronAPIhas 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.tsexposure, the system handler set, the CQRS dispatcher, or the error path. Renderer-sideElectronBridgeServicespec status not verified.
Security considerations
contextIsolation: true+ nonodeIntegrationin the renderer;electron/preload.tsis the only crossing.- Adding a channel requires touching both
preload.tsandelectron/ipc/. There is no dynamic channel registration. - File-system handlers should validate paths against user-data scope — TODO: audit
system.tsfor path-traversal protections beyond what the plugin loader does. - Deep-link handling:
consumePendingDeepLinkreturns 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 singlereadFileto 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
windowglobal iselectronAPI, notapi. CONTEXT.md useswindow.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 — 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
getSourcesand the Linux audio routing methods for screen-share media capture.
Changelog
| Date | Change |
|---|---|
| 2026-05-25 | Initial documentation |