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

> Discover how AionUi secures IPC communication between Electron main and renderer processes using contextBridge. Learn about request-reply and broadcast patterns.

- Repository: [OfficeAI/AionUi](https://github.com/iofficeai/aionui)
- Tags: deep-dive
- Published: 2026-02-19

---

**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`](https://github.com/iOfficeAI/AionUi/blob/main/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.

```typescript
// 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`](https://github.com/iOfficeAI/AionUi/blob/main/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:

```typescript
// 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:

```typescript
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:

```typescript
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 Processing** – `ipcMain.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:

```tsx
// 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:

```tsx
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`](https://github.com/iOfficeAI/AionUi/blob/main/src/preload.ts) with new `ipcRenderer.invoke` calls, then implement corresponding `ipcMain.handle` listeners in [`src/adapter/main.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/adapter/main.ts).

## Summary

- **Secure Isolation** – AionUi uses `contextBridge` in [`src/preload.ts`](https://github.com/iOfficeAI/AionUi/blob/main/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`](https://github.com/iOfficeAI/AionUi/blob/main/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`](https://github.com/iOfficeAI/AionUi/blob/main/src/preload.ts).

### How does AionUi route messages to multiple open windows?

The `initMainAdapterWithWindow` function in [`src/adapter/main.ts`](https://github.com/iOfficeAI/AionUi/blob/main/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`](https://github.com/iOfficeAI/AionUi/blob/main/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.