How AionUi Implements IPC Communication Between Electron Main and Renderer Processes Using contextBridge

AionUi isolates the renderer process from Node.js APIs by exposing a strictly typed electronAPI through Electron's contextBridge, enabling secure two-way IPC via the office-ai-bridge-adapter channel that supports both request-reply invocations and server-push broadcasts.

The iOfficeAI/AionUi repository demonstrates a production-ready pattern for securing Electron applications. By implementing a custom IPC communication layer, the codebase ensures the React-based renderer cannot directly access Electron internals while maintaining efficient bi-directional data flow with the main process.

Renderer-Side API Exposure via src/preload.ts

AionUi's preload script acts as the sole security gateway between the sandboxed UI and the privileged main process. Located at src/preload.ts, this file uses contextBridge.exposeInMainWorld to inject a controlled API surface into the global window object.

Secure API Surface with contextBridge

The contextBridge module creates a read-only, frozen electronAPI object that the renderer can access via window.electronAPI. This pattern prevents prototype pollution and blocks unauthorized access to Node.js modules from the frontend code.

// src/preload.ts
import { contextBridge, ipcRenderer, webUtils } from 'electron';
import { ADAPTER_BRIDGE_EVENT_KEY } from './adapter/constant';

contextBridge.exposeInMainWorld('electronAPI', {
  emit: (name: string, data: any) =>
    ipcRenderer
      .invoke(ADAPTER_BRIDGE_EVENT_KEY, JSON.stringify({ name, data }))
      .catch((error) => {
        console.error('IPC invoke error:', error);
        throw error;
      }),

  on: (callback: any) => {
    const handler = (event: any, value: any) => callback({ event, value });
    ipcRenderer.on(ADAPTER_BRIDGE_EVENT_KEY, handler);
    return () => ipcRenderer.off(ADAPTER_BRIDGE_EVENT_KEY, handler);
  },

  getPathForFile: (file: File) => webUtils.getPathForFile(file),
  webuiResetPassword: () => ipcRenderer.invoke('webui-direct-reset-password'),
});

Request-Reply and Broadcast Patterns

The exposed API implements two distinct communication models:

  • emit – Utilizes ipcRenderer.invoke for asynchronous request-reply patterns. The method serializes the payload to JSON and returns a Promise that resolves with the main process's response.
  • on – Registers a persistent listener using ipcRenderer.on to receive broadcast messages initiated by the main process. It returns an unsubscribe function to prevent memory leaks during component unmounting.

Main-Process IPC Handling in src/adapter/main.ts

The main process implements the corresponding handlers in src/adapter/main.ts, routing messages through a platform-level abstraction while managing multiple BrowserWindows.

Bridge Registration with ipcMain.handle

The bridge.adapter function registers callbacks that wire into Electron's ipcMain module. The on callback establishes the request handler using ipcMain.handle for the ADAPTER_BRIDGE_EVENT_KEY channel:

// src/adapter/main.ts
import { ipcMain, BrowserWindow } from 'electron';
import { bridge } from '@office-ai/platform';
import { ADAPTER_BRIDGE_EVENT_KEY } from './constant';

interface BridgeEventData { name: string; data: unknown }

bridge.adapter({
  emit(name, data) {
    // Broadcasting logic handled here
  },

  on(emitter) {
    ipcMain.handle(ADAPTER_BRIDGE_EVENT_KEY, (_event, info) => {
      const { name, data } = JSON.parse(info) as BridgeEventData;
      return Promise.resolve(emitter.emit(name, data));
    });
  },
});

When the renderer calls window.electronAPI.emit(), this handler deserializes the JSON payload and forwards it to the platform's internal event emitter, returning a resolved Promise back to the renderer.

Multi-Window Broadcasting and WebSocket Integration

The emit callback within bridge.adapter handles outbound communication, fanning out messages to all registered renderer windows and WebSocket clients:

const adapterWindowList: BrowserWindow[] = [];
const webSocketBroadcasters: ((name:string,data:unknown)=>void)[] = [];

bridge.adapter({
  emit(name, data) {
    for (const win of adapterWindowList) {
      win.webContents.send(
        ADAPTER_BRIDGE_EVENT_KEY,
        JSON.stringify({ name, data })
      );
    }
    for (const broadcast of webSocketBroadcasters) {
      try { broadcast(name, data); }
      catch (e) { console.error('[MainAdapter] WebSocket broadcast error:', e); }
    }
  },
  on(emitter) { /* ... */ },
});

The adapterWindowList array tracks all active BrowserWindow instances. The helper function initMainAdapterWithWindow manages this registry, automatically removing closed windows to prevent invalid webContents.send operations:

export const initMainAdapterWithWindow = (win: BrowserWindow) => {
  adapterWindowList.push(win);
  const off = () => {
    const idx = adapterWindowList.indexOf(win);
    if (idx > -1) adapterWindowList.splice(idx, 1);
  };
  win.on('closed', off);
  return off;
};

IPC Message Flow Architecture

The complete communication cycle follows a consistent four-step flow using the single office-ai-bridge-adapter channel:

  1. Renderer Request – The React component calls window.electronAPI.emit(), triggering ipcRenderer.invoke with a serialized JSON payload.
  2. Main ProcessingipcMain.handle receives the payload in the main process, parses it, and routes it through the platform's bridge.adapter emitter.
  3. Main Broadcast – When the platform emits events, the bridge.adapter.emit callback forwards data to all windows via win.webContents.send and to WebSocket broadcasters.
  4. Renderer Reception – The preload's on method routes these broadcasts through ipcRenderer.on, invoking the React component's registered callback with the deserialized event data.

Practical Implementation Examples

Sending Commands from React Components

To invoke main-process functionality from the UI, access the typed global API:

// Triggering a password reset flow
function handleReset() {
  window.electronAPI.webuiResetPassword()
    .then(() => console.log('Password reset initiated'))
    .catch(err => console.error('Reset failed:', err));
}

// Emitting a custom platform event
async function openFileDialog() {
  const result = await window.electronAPI.emit('open-file-dialog', {
    filters: [{ name: 'Images', extensions: ['png', 'jpg'] }]
  });
  console.log('Selected files:', result);
}

Listening for Broadcast Events

Subscribe to main-process pushes using the on method, ensuring proper cleanup:

import { useEffect } from 'react';

useEffect(() => {
  const unsubscribe = window.electronAPI.on(({ event, value }) => {
    const payload = JSON.parse(value);
    console.log('Broadcast received:', payload.name, payload.data);
  });
  
  return unsubscribe; // Removes ipcRenderer listener on unmount
}, []);

Extending the Preload API

To expose additional main-process capabilities, extend the contextBridge.exposeInMainWorld object in src/preload.ts with new ipcRenderer.invoke calls, then implement corresponding ipcMain.handle listeners in src/adapter/main.ts.

Summary

  • Secure Isolation – AionUi uses contextBridge in src/preload.ts to expose a minimal, typed electronAPI global, preventing renderer access to Node.js internals.
  • Unified Channel – All IPC traffic flows through the ADAPTER_BRIDGE_EVENT_KEY constant (office-ai-bridge-adapter), simplifying debugging and monitoring.
  • Dual Patterns – The architecture supports both request-reply (invoke/handle) and pub-sub (send/on) communication models through the same channel abstraction.
  • Multi-Window Support – The adapterWindowList registry in src/adapter/main.ts ensures broadcasts reach every active BrowserWindow while automatically cleaning up closed instances.
  • WebSocket Bridge – The main adapter integrates with WebSocket servers, allowing external clients to participate in the same event bus as Electron renderers.

Frequently Asked Questions

What is the security benefit of using contextBridge in AionUi?

According to the AionUi source code, contextBridge.exposeInMainWorld creates a readonly, tamper-proof API surface that prevents the React renderer from accessing ipcRenderer directly. This isolation ensures that even if the frontend executes untrusted code, it cannot invoke arbitrary main-process methods or access the filesystem without explicit whitelisting in src/preload.ts.

How does AionUi route messages to multiple open windows?

The initMainAdapterWithWindow function in src/adapter/main.ts maintains an adapterWindowList array of all active BrowserWindow instances. When the platform emits a broadcast, the bridge.adapter.emit callback iterates through this array and calls win.webContents.send for each window, ensuring synchronized state across the entire application.

What is the difference between emit and on in the electronAPI?

The emit method implements a request-reply pattern using ipcRenderer.invoke, returning a Promise that resolves with data from the main process. The on method implements a subscription pattern using ipcRenderer.on, allowing the renderer to receive asynchronous pushes from the main process without requesting them, and returns a cleanup function to prevent memory leaks.

Where is the IPC channel name defined in AionUi?

The canonical channel identifier office-ai-bridge-adapter is defined as the constant ADAPTER_BRIDGE_EVENT_KEY in src/adapter/constant.ts. Both the preload script and the main adapter import this constant, ensuring the renderer and main process communicate on the same channel without hardcoding strings in multiple files.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →