How the Renderer Communicates with the Main Process in Munder Difflin: Complete IPC Guide

Munder Difflin uses a secure preload bridge with Electron's contextBridge and ipcRenderer.invoke/ipcMain.handle pattern to let the renderer process call main-process operations while maintaining strict sandbox isolation.

This architecture follows Electron's security best practices by exposing a typed, minimal API surface to the UI. The renderer process never accesses Node.js or system APIs directly—instead, all privileged operations flow through an explicitly defined bridge in src/preload/index.ts.

The Preload Bridge: Controlled API Exposure

The preload script (src/preload/index.ts) runs in a privileged context with access to both Electron APIs and the DOM. It uses contextBridge.exposeInMainWorld to inject a safe, typed API that the renderer can access via window.api.

The bridge provides two communication patterns:

  • ipcRenderer.invoke – for request/response calls that return promises
  • ipcRenderer.on/ipcRenderer.send – for event-driven, bidirectional streaming

Key Bridge Methods

Method IPC Channel Purpose
hiveBoard() hive:board Fetches current Hive board state
onHiveMessage(listener) hive:message Subscribes to real-time Hive messages
ptySpawn(opts) pty:spawn Spawns a new pseudo-terminal
gitStatus() git:status Retrieves Git repository status

Source: [src/preload/index.ts](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/preload/index.ts)

// src/preload/index.ts
export const api = {
  // Request/response pattern
  hiveBoard: () => ipcRenderer.invoke('hive:board'),
  ptySpawn: (opts: PtyOptions) => ipcRenderer.invoke('pty:spawn', opts),

  // Event subscription pattern with cleanup
  onHiveMessage: (listener: (msg: HiveMessage) => void) => {
    ipcRenderer.on('hive:message', listener);
    return () => ipcRenderer.removeListener('hive:message', listener);
  },
};

contextBridge.exposeInMainWorld('api', api);

Main-Process Handlers: Executing Privileged Operations

The main process (src/main/index.ts) registers handlers using ipcMain.handle for async invoke calls and ipcMain.on for incoming events. These handlers perform system-level work that the sandboxed renderer cannot access directly—including file system operations, PTY management, and Hive state updates.

Source: [src/main/index.ts](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/index.ts)

// src/main/index.ts
import { ipcMain, BrowserWindow } from 'electron';
import { hive } from './hive';

// Handle renderer requests
ipcMain.handle('hive:board', () => hive.board());

ipcMain.handle('pty:spawn', async (event, opts: PtyOptions) => {
  // Spawn PTY with system privileges unavailable to renderer
  const pty = new PTY(opts);
  return pty.id;
});

// Broadcast events to all renderer windows
ipcMain.on('hive:broadcast', (event, msg) => {
  BrowserWindow.getAllWindows().forEach(win => {
    win.webContents.send('hive:message', msg);
  });
});

The main process acts as a central authority for all privileged operations, enforcing security boundaries while enabling rich desktop functionality.

Renderer Usage: Clean Async API Surface

In the UI layer (React/Vue components), the renderer accesses the bridge through the globally exposed window.api object. This abstraction hides all IPC complexity—methods appear as standard async functions with TypeScript support.

Source: [src/renderer/src/store/store.ts](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/renderer/src/store/store.ts)

// src/renderer/src/store/store.ts
async function loadHiveBoard() {
  // Automatically becomes ipcRenderer.invoke('hive:board')
  const board = await window.api.hiveBoard();
  store.set({ board });
}

// Subscribe with automatic cleanup
function subscribeToMessages() {
  const unsubscribe = window.api.onHiveMessage(message => {
    store.updateMessages(message);
  });
  
  // Cleanup on unmount
  return unsubscribe;
}

The renderer code remains pure frontend logic—no Electron imports, no direct IPC knowledge, and fully testable outside the Electron environment.

Communication Patterns Compared

Munder Difflin implements two distinct IPC patterns based on use case requirements:

Request/Response (invoke/handle)

  • Use: Single-shot data fetching, command execution
  • Example: hiveBoard(), ptySpawn(), gitStatus()
  • Guarantees: Promise-based, automatic error propagation

Event-Driven (on/send)

  • Use: Streaming data, real-time updates, broadcasts
  • Example: onHiveMessage(), terminal output streams
  • Guarantees: Manual subscription cleanup, multicast to windows

Both patterns are fully typed through TypeScript interfaces defined in the preload script, ensuring compile-time safety across process boundaries.

Summary

  • Preload bridge (src/preload/index.ts) exposes a minimal, typed API via contextBridge to maintain renderer sandbox security
  • Main handlers (src/main/index.ts) register ipcMain.handle and ipcMain.on callbacks for all privileged operations
  • Renderer code calls window.api methods as standard async functions without direct Electron dependencies
  • Two patterns: invoke/handle for request/response, on/send for streaming events
  • Security model: Renderer has zero Node.js or system access; all operations flow through explicit, auditable bridge definitions

Frequently Asked Questions

How does Munder Difflin prevent the renderer from accessing unsafe APIs?

The preload script uses Electron's contextBridge.exposeInMainWorld to whitelist only specific methods. The renderer runs with contextIsolation: true (Electron's default), meaning it cannot access require, Node.js modules, or Electron APIs directly. Every system interaction must pass through the explicitly defined bridge in src/preload/index.ts.

What's the difference between ipcRenderer.invoke and ipcRenderer.send?

ipcRenderer.invoke is used with ipcMain.handle for request/response patterns—it returns a Promise with the main process result. ipcRenderer.send is used with ipcMain.on for fire-and-forget or event streaming—it has no return value and is typically paired with ipcRenderer.on for bidirectional communication. Munder Difflin uses invoke for Hive board fetches and on/send for real-time message streams.

Can renderer-to-main calls timeout or fail?

Yes. Since ipcRenderer.invoke returns a Promise, it can reject if the main handler throws or if Electron's internal IPC mechanism fails. The preload bridge in src/preload/index.ts does not add automatic retry logic—renderer code should handle errors using standard try/catch or .catch() patterns on the returned Promise.

Where is the API type definition for window.api?

Type definitions are co-located in the preload script or a shared types file imported by both preload and renderer. This ensures TypeScript intellisense in src/renderer/src/store/store.ts while maintaining runtime safety through contextBridge. The global window.api type is typically declared via a TypeScript interface augmentation in the renderer's type declarations.

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 →