# How Rowboat Implements Secure Electron IPC Communication Between Main and Renderer Processes

> Learn how Rowboat uses a type safe preload bridge for secure Electron IPC communication between main and renderer processes. Discover validated ipcRenderer methods and ipcMain handler registration.

- Repository: [RowBoat Labs/rowboat](https://github.com/rowboatlabs/rowboat)
- Tags: internals
- Published: 2026-02-16

---

**Rowboat implements Electron IPC communication through a type-safe preload bridge that exposes validated `ipcRenderer` methods to the renderer, while the main process registers handlers via `ipcMain` to execute privileged operations securely.**

Rowboat, an open-source project by rowboatlabs, implements a robust **Electron IPC communication** architecture that isolates privileged main-process APIs from the renderer while maintaining strict type safety. The implementation uses a preload script bridge combined with Zod schema validation to ensure secure, validated data transfer between the main and renderer processes.

## The Architecture of Rowboat's Electron IPC Communication

The IPC system consists of two primary components: the **preload bridge** that exposes a controlled API to the renderer, and the **main process handlers** that process those requests. This architecture ensures that the renderer process never accesses Node.js or Electron APIs directly, adhering to Electron's security best practices.

### Preload Script Bridge ([`apps/x/apps/preload/src/preload.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/preload/src/preload.ts))

The preload script runs in an isolated context with access to both the renderer's `window` object and Node.js APIs. In [`apps/x/apps/preload/src/preload.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/preload/src/preload.ts), Rowboat uses `contextBridge.exposeInMainWorld` to publish an `ipc` object containing four methods:

- `invoke`: Wraps `ipcRenderer.invoke` for request-response calls
- `send`: Wraps `ipcRenderer.send` for fire-and-forget events  
- `on`: Wraps `ipcRenderer.on` for event subscriptions
- `removeListener`: Wraps `ipcRenderer.removeListener` for cleanup

All arguments are validated via shared Zod schemas before being sent to the main process, guaranteeing type-safety and preventing arbitrary data from crossing the boundary.

### Main Process Handlers ([`apps/x/apps/main/src/ipc.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/main/src/ipc.ts))

The main process registers IPC handlers in [`apps/x/apps/main/src/ipc.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/main/src/ipc.ts) using Electron's `ipcMain` module. The implementation distinguishes between two communication patterns:

- **`ipcMain.handle`**: Registers async handlers for `invoke` calls, returning Promises that resolve in the renderer
- **`ipcMain.on`**: Listens for `send` events and executes callbacks without returning values to the renderer

Each handler validates incoming arguments using the same Zod schemas as the preload bridge, performs the requested business logic (such as file system access or AI provider calls), and optionally replies using `event.reply` or returns a value for `invoke`. The main process can also send unsolicited events back to the renderer via `webContents.send`, which the preload bridge forwards to the subscribed renderer listeners.

## Implementing the Preload Bridge for Secure IPC

The preload script serves as the security gatekeeper between the untrusted renderer and the privileged main process. In [`apps/x/apps/preload/src/preload.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/preload/src/preload.ts), Rowboat implements a typed bridge that validates all data before it reaches `ipcRenderer`.

```typescript
import { contextBridge, ipcRenderer } from 'electron';
import { validateArgs } from '@x/shared/validators';

contextBridge.exposeInMainWorld('ipc', {
  invoke: (channel: string, args: unknown) => {
    const validated = validateArgs(channel, args);
    return ipcRenderer.invoke(channel, validated);
  },
  send: (channel: string, args: unknown) => {
    const validated = validateArgs(channel, args);
    ipcRenderer.send(channel, validated);
  },
  on: (channel: string, listener: (event: any, ...args: any[]) => void) => {
    ipcRenderer.on(channel, listener);
  },
  removeListener: (channel: string, listener: any) => {
    ipcRenderer.removeListener(channel, listener);
  },
});

```

This implementation ensures that every IPC call passes through `validateArgs`, which uses Zod schemas defined in `apps/x/packages/shared/src/validators` to enforce type safety. By exposing only specific methods rather than the entire `ipcRenderer` module, Rowboat prevents the renderer from accessing dangerous Electron APIs directly.

## Registering IPC Handlers in the Main Process

The main process in Rowboat registers IPC handlers in [`apps/x/apps/main/src/ipc.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/main/src/ipc.ts), creating the server-side counterpart to the preload bridge. This file uses `ipcMain` to listen for messages from the renderer and execute privileged operations such as file system access or AI provider integrations.

```typescript
import { ipcMain, BrowserWindow } from 'electron';
import { getUserProfile } from './services/user';

// Handle request-response pattern
ipcMain.handle('user:getProfile', async (_event, { userId }) => {
  // Business logic runs in the privileged main process
  const profile = await getUserProfile(userId);
  return profile; // Returned value resolves the renderer's invoke promise
});

// Handle fire-and-forget events
ipcMain.on('app:logEvent', (_event, data) => {
  console.log('Event from renderer:', data);
  // No return value expected
});

```

The main process can also push unsolicited events to the renderer using `webContents.send` on a specific `BrowserWindow` instance. When the main process needs to broadcast events (such as progress updates or external notifications), it retrieves the `webContents` instance and calls `send`, which the preload bridge forwards to subscribed listeners in the renderer.

## Consuming IPC in the Renderer Process

Renderer code in Rowboat accesses the IPC API through the `window.ipc` object exposed by the preload script. This provides a clean, Promise-based interface for calling main-process functionality while maintaining the security boundaries required by Electron's context isolation.

```tsx
// In a React component running in the renderer
import { useEffect } from 'react';

function fetchUserProfile() {
  // window.ipc.invoke is the API exposed by the preload bridge
  window.ipc.invoke('user:getProfile', { userId: '123' })
    .then(profile => console.log(profile))
    .catch(err => console.error(err));
}

// Listening for async events sent from the main process
useEffect(() => {
  const onUpdate = (_event, data) => console.log('Profile update:', data);
  window.ipc.on('user:profileUpdated', onUpdate);
  return () => window.ipc.removeListener('user:profileUpdated', onUpdate);
}, []);

```

This pattern ensures that renderer code never directly imports Electron modules, adhering to Electron's security best practices. The `window.ipc` API provides four methods—`invoke`, `send`, `on`, and `removeListener`—that map directly to the preload bridge implementation, creating a type-safe contract between the renderer and main processes.

## Summary

- Rowboat implements **Electron IPC communication** through a preload bridge in [`apps/x/apps/preload/src/preload.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/preload/src/preload.ts) that exposes a validated API to the renderer via `contextBridge.exposeInMainWorld`.
- The preload script wraps `ipcRenderer.invoke`, `send`, `on`, and `removeListener`, validating all arguments with Zod schemas before transmission.
- Main process handlers in [`apps/x/apps/main/src/ipc.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/main/src/ipc.ts) register listeners via `ipcMain.handle` and `ipcMain.on` to execute privileged operations like file system access or AI provider calls.
- The main process can push unsolicited updates to the renderer using `webContents.send`, which the preload bridge forwards to subscribed listeners.
- Renderer code accesses the IPC API through `window.ipc`, maintaining security boundaries while enabling type-safe communication between processes.

## Frequently Asked Questions

### How does Rowboat ensure type safety in Electron IPC communication?

Rowboat ensures type safety by validating all IPC payloads with Zod schemas defined in `apps/x/packages/shared/src/validators`. Both the preload bridge and main process handlers use these shared schemas to validate arguments before processing, preventing type errors and ensuring that only properly structured data crosses the process boundary.

### What is the difference between `invoke` and `send` in Rowboat's IPC implementation?

In Rowboat's implementation, `invoke` creates a request-response pattern where the main process handler registered with `ipcMain.handle` returns a Promise that resolves in the renderer, making it ideal for data retrieval operations. Conversely, `send` uses a fire-and-forget pattern where the renderer emits an event without expecting a return value, handled by `ipcMain.on` in the main process, suitable for logging or event tracking.

### How does the main process push updates to the renderer without a request?

The main process sends unsolicited events to the renderer using `webContents.send` on a specific `BrowserWindow` instance. When the main process needs to broadcast events such as progress updates or external notifications, it retrieves the `webContents` instance and calls `send`, which the preload bridge forwards to any subscribed listeners in the renderer via the `window.ipc.on` interface.

### Where are the Zod validation schemas defined in the Rowboat codebase?

The Zod validation schemas are defined in the shared package at `apps/x/packages/shared/src/validators`. These schemas are imported by both the preload script ([`apps/x/apps/preload/src/preload.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/preload/src/preload.ts)) and the main process handlers ([`apps/x/apps/main/src/ipc.ts`](https://github.com/rowboatlabs/rowboat/blob/main/apps/x/apps/main/src/ipc.ts)) to ensure consistent validation across the IPC boundary.