# How OpenScreen's Electron IPC Bridge Enables Secure Main-Renderer Communication

> Discover how OpenScreen's Electron IPC bridge uses contextBridge and ipcRenderer to securely connect React renderer to Node.js functionality in the main process.

- Repository: [Sid/openscreen](https://github.com/siddharthvaddem/openscreen)
- Tags: internals
- Published: 2026-04-03

---

**OpenScreen implements Electron's secure contextBridge pattern to expose a curated `window.electronAPI` interface, allowing the React renderer to safely invoke Node.js functionality in the main process through structured `ipcRenderer.invoke` and `ipcMain.handle` channels.**

OpenScreen is an open-source screen recording application that leverages Electron's architecture to isolate privileged system operations from the user interface. The Electron IPC bridge serves as the critical communication layer between the Node.js-powered main process and the Chromium-based renderer, ensuring that sensitive APIs like file system access and screen capture remain secure while remaining accessible to React components.

## The Preload Script: Building the Secure API Surface

The IPC bridge originates in [`electron/preload.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/preload.ts), where the preload script uses `contextBridge.exposeInMainWorld` to create a controlled entry point for the renderer.

```ts
// electron/preload.ts
import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  // fire-and-forget
  hudOverlayHide: () => ipcRenderer.send("hud-overlay-hide"),

  // request-response (async)
  getSources: async (opts: Electron.SourcesOptions) =>
    await ipcRenderer.invoke("get-sources", opts),

  // listeners with cleanup
  onStopRecordingFromTray: (callback: () => void) => {
    const listener = () => callback();
    ipcRenderer.on("stop-recording-from-tray", listener);
    return () => ipcRenderer.removeListener("stop-recording-from-tray", listener);
  },

  storeRecordedVideo: (buffer: ArrayBuffer, fileName: string) =>
    ipcRenderer.invoke("store-recorded-video", buffer, fileName),
});

```

The preload script runs in an isolated context with full Node.js access, while the renderer operates with `nodeIntegration: false`. By explicitly exposing only specific methods via `contextBridge`, the Electron IPC bridge prevents the renderer from accessing arbitrary OS APIs, maintaining strict security boundaries.

**Key distinction between methods:**
- **`invoke`** creates a Promise-based request/response pattern expecting data return
- **`send`** provides fire-and-forget messaging for one-way commands

## Main Process Handlers: Implementing Privileged Operations

The main process registers all IPC handlers in [`electron/ipc/handlers.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/ipc/handlers.ts), centralizing heavy-weight logic including file I/O, OS dialogs, and screen capture via `desktopCapturer`.

```ts
// electron/ipc/handlers.ts
export function registerIpcHandlers(
  createEditorWindow,
  createSourceSelectorWindow,
  getMainWindow,
  getSourceSelectorWindow,
  onRecordingStateChange,
) {
  // Request/response pattern
  ipcMain.handle("get-sources", async (_, opts) => {
    const sources = await desktopCapturer.getSources(opts);
    return sources.map(s => ({
      id: s.id,
      name: s.name,
      display_id: s.display_id,
      thumbnail: s.thumbnail?.toDataURL() ?? null,
      appIcon: s.appIcon?.toDataURL() ?? null,
    }));
  });

  ipcMain.handle("select-source", (_, source) => {
    selectedSource = source;
    const win = getSourceSelectorWindow();
    if (win) win.close();
    return selectedSource;
  });

  // Fire-and-forget pattern
  ipcMain.on("hud-overlay-hide", () => {
    const win = getMainWindow();
    if (win) win.hide();
  });
}

```

These handlers are registered once during application startup in [`electron/main.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/main.ts):

```ts
// electron/main.ts
app.whenReady().then(async () => {
  registerIpcHandlers(
    createEditorWindowWrapper,
    createSourceSelectorWindowWrapper,
    () => mainWindow,
    () => sourceSelectorWindow,
    (recording, sourceName) => {
      selectedSourceName = sourceName;
      updateTrayMenu(recording);
      if (!recording) showMainWindow();
    },
  );
});

```

The main handlers maintain module-level state (such as `selectedSource` and `currentProjectPath`) that persists across renderer reloads while remaining completely inaccessible to the renderer process directly.

## Renderer Consumption: React Components Using the Bridge

Renderer components access the Electron IPC bridge through the typed global `window.electronAPI` object, without importing any Electron modules directly. The [`src/components/launch/SourceSelector.tsx`](https://github.com/siddharthvaddem/openscreen/blob/main/src/components/launch/SourceSelector.tsx) component demonstrates the typical usage pattern:

```tsx
// src/components/launch/SourceSelector.tsx
const fetchSources = async () => {
  const rawSources = await window.electronAPI.getSources({
    types: ["screen", "window"],
    thumbnailSize: { width: 320, height: 180 },
  });
  setSources(rawSources);
};

const pickSource = async (source) => {
  await window.electronAPI.selectSource(source);
  // Main process closes the source-selector window
};

```

For events originating from the main process—such as tray menu interactions—components subscribe using the cleanup functions returned by the preload:

```tsx
// Listening for main-process events
useEffect(() => {
  const unsubscribe = window.electronAPI.onStopRecordingFromTray(() => {
    stopRecording();
  });
  return unsubscribe; // Cleanup on unmount
}, []);

```

## Security Architecture and Context Isolation

OpenScreen's Electron IPC bridge implements defense-in-depth through multiple security mechanisms:

1. **Context Isolation**: The renderer runs with `contextIsolation: true`, ensuring it cannot directly access Node.js APIs or the preload script's internal variables.

2. **Node Integration Disabled**: With `nodeIntegration: false`, the renderer cannot require Node modules, preventing arbitrary code execution.

3. **Permission Validation**: The main process validates arguments before performing operations, using helpers like `normalizeVideoSourcePath` and `isTrustedProjectPath` before touching the filesystem.

4. **Structured Error Handling**: All IPC handlers catch exceptions and return `{ success: false, error: ... }` objects rather than crashing the bridge, ensuring resilience across process boundaries.

5. **Session Permissions**: `session.defaultSession.setPermissionCheckHandler` restricts media-related permissions, preventing privilege escalation from the renderer.

## Practical Code Examples

### Requesting Screen Sources (Renderer to Main)

This pattern demonstrates the async request/response flow for retrieving available screen capture sources:

```tsx
// In a React component
async function loadScreenSources() {
  const sources = await window.electronAPI.getSources({
    types: ["screen"],
    thumbnailSize: { width: 200, height: 150 },
  });
  console.log("Available screens:", sources);
}

```

**Bridge flow:**
1. `window.electronAPI.getSources` invokes `ipcRenderer.invoke('get-sources', opts)` in the preload
2. `ipcMain.handle('get-sources', ...)` executes in the main process, calling `desktopCapturer.getSources`
3. Mapped data returns through the Promise chain to the React component

### Saving Recorded Video with Binary Data

```tsx
function saveVideo(blob: Blob, fileName: string) {
  blob.arrayBuffer().then(buf => {
    window.electronAPI.storeRecordedVideo(buf, fileName).then(result => {
      if (result.success) {
        console.log("Saved to:", result.path);
      }
    });
  });
}

```

The main handler receives the `ArrayBuffer`, writes it to `RECORDINGS_DIR` via Node.js `fs` APIs, and returns a status object containing the final path.

### Tray-Initiated Commands (Main to Renderer)

When users interact with the system tray, the main process emits events to the renderer:

```tsx
// Main process (electron/main.ts)
mainWindow.webContents.send('stop-recording-from-tray');

// Renderer receives via preload bridge
window.electronAPI.onStopRecordingFromTray(callback);

```

## Summary

- **Secure Exposure**: The [`electron/preload.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/preload.ts) script uses `contextBridge.exposeInMainWorld` to create a limited `window.electronAPI` surface, preventing renderer access to arbitrary Node.js functionality.
- **Handler Registration**: [`electron/ipc/handlers.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/ipc/handlers.ts) registers all `ipcMain.handle` and `ipcMain.on` methods, centralizing privileged operations like `desktopCapturer.getSources` and file system access.
- **Async Patterns**: The bridge supports both Promise-based `invoke/handle` patterns for data retrieval and `send/on` patterns for fire-and-forget commands.
- **Event Broadcasting**: Main-to-renderer communication uses `webContents.send` paired with `ipcRenderer.on` listeners that expose cleanup functions for proper React lifecycle management.
- **Isolation Enforcement**: `contextIsolation: true` and `nodeIntegration: false` ensure that only explicitly exposed APIs cross the process boundary, with validation occurring in the main process before any system operations execute.

## Frequently Asked Questions

### How does OpenScreen prevent the renderer from accessing dangerous Node.js APIs?

OpenScreen disables `nodeIntegration` and enables `contextIsolation` in its BrowserWindow configuration. The Electron IPC bridge only exposes specific functions through `contextBridge.exposeInMainWorld` in [`electron/preload.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/preload.ts), creating a whitelist of safe operations. The renderer can only call `window.electronAPI` methods and cannot require Node modules or access the file system directly.

### What is the difference between `ipcRenderer.invoke` and `ipcRenderer.send` in OpenScreen?

`ipcRenderer.invoke` creates a Promise-based request/response pattern used for operations that return data, such as `getSources` which retrieves screen capture sources. `ipcRenderer.send` provides fire-and-forget messaging for one-way commands like `hudOverlayHide` where no response is expected. The main process handles invocations with `ipcMain.handle` and one-way messages with `ipcMain.on`.

### How does OpenScreen handle cleanup of IPC event listeners?

The preload script in [`electron/preload.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/preload.ts) returns cleanup functions when registering listeners. For example, `onStopRecordingFromTray` returns a function that calls `ipcRenderer.removeListener`. React components in the renderer use these cleanup functions in `useEffect` return hooks to prevent memory leaks when components unmount.

### Where are the IPC handlers registered in the OpenScreen codebase?

All IPC handlers are defined in [`electron/ipc/handlers.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/ipc/handlers.ts) and registered through the `registerIpcHandlers` function. This function is called once during application initialization in [`electron/main.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/main.ts) within the `app.whenReady()` promise, ensuring handlers are available before any renderer windows load.