# How Inter-Process Communication (IPC) Works Between Main and Renderer Processes in Prompt Optimizer’s Electron App

> Explore how Inter-Process Communication (IPC) works in Electron apps. Discover Prompt Optimizer's service-proxy architecture using contextBridge and ipcRenderer for type-safe communication between main and renderer processes.

- Repository: [且炼时光/prompt-optimizer](https://github.com/linshenkx/prompt-optimizer)
- Tags: internals
- Published: 2026-02-23

---

**Prompt Optimizer uses a service-proxy architecture where the main process registers `ipcMain.handle` listeners to expose Node.js core services, while the preload script bridges these to the renderer via `contextBridge`, enabling type-safe IPC through `ipcRenderer.invoke`.**

The `linshenkx/prompt-optimizer` repository ships an Electron-based desktop client that requires secure communication between the Chromium-based UI and native Node.js capabilities. Understanding how Inter-Process Communication (IPC) works between the main and renderer processes is essential for developers extending the desktop functionality or debugging service interactions.

## IPC Architecture Overview

The application follows a classic **high-level service-proxy** architecture that separates privileged Node.js operations from the sandboxed web environment.

### Main Process as the IPC Server

Located in [`packages/desktop/main.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/main.js), the main process acts as the **server**. It instantiates the real core services—such as `LLMService` and `ModelManager`—inside the Node.js environment. It then registers request handlers using `ipcMain.handle` for each exposed operation.

### Preload Script as the Security Gateway

The [`packages/desktop/preload.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/preload.js) script runs in a sandboxed context with limited Node.js access. It uses `contextBridge.exposeInMainWorld` to export a typed `electronAPI` object. This object forwards calls to the main process via `ipcRenderer.invoke`, acting as a secure **gateway** that prevents direct access to unsafe APIs.

### Renderer Process as the IPC Client

The renderer process hosts the Vue-based UI inside Chromium. It acts as the **client** by invoking methods on `window.electronAPI`. These calls are routed through the preload bridge to the main process, allowing the UI to execute Node.js operations as if they were local asynchronous functions.

## Data Flow from UI to Core Services

The request-response cycle follows five distinct steps:

1. **User interaction** in a Vue component triggers a call such as `window.electronAPI.llm.testConnection(provider)`.
2. The **preload script** receives the call and executes `ipcRenderer.invoke('llm-testConnection', provider)`.
3. The **main process** matches the channel with `ipcMain.handle('llm-testConnection', async (event, provider) => { … })`. It forwards the request to the real `LLMService` instance, which performs the Node.js operation (e.g., a `fetch` request).
4. The result—returned as plain JSON, never as a raw `Response` object—is serialized back through the IPC channel to the preload script, resolving the `invoke` promise.
5. The **renderer** receives the data and updates the Vue UI accordingly.

Because `ipcRenderer.invoke` and `ipcMain.handle` implement a **request-response** pattern, the code appears synchronous when using `async/await`, and Electron automatically handles data marshalling between processes.

## Why the Proxy Pattern Is Used

Directly importing `@prompt-optimizer/core` modules inside the renderer would create a **second, isolated** instance of core services, breaking data sharing and preventing the use of Node-only APIs such as the file system or native modules.

The **proxy objects**—`ElectronLLMProxy`, `ElectronModelManagerProxy`, and others—expose identical method signatures to the core services but internally forward every call through IPC. This ensures there is **exactly one source of truth**: the main-process instance running in Node.js.

## Safety Guarantees and Type Safety

The IPC implementation provides multiple layers of protection:

* **Context isolation** – The renderer never accesses `require` or raw Node APIs. All privileged actions are mediated by the preload script.
* **Type-safe API** – Global types are declared in [`packages/ui/src/types/electron.d.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/types/electron.d.ts), enabling IDE autocomplete and compile-time checking for `window.electronAPI`.
* **Structured error handling** – Handlers wrap service calls in `try/catch` blocks and return `{ success: false, error: … }` objects. This prevents uncaught exceptions from crossing process boundaries and crashing the application.

## Implementation Examples

### Registering Handlers in the Main Process

The main process sets up request handlers during initialization in [`packages/desktop/main.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/main.js):

```typescript
// packages/desktop/main.js
import { ipcMain } from 'electron';
import { createLLMService } from '@prompt-optimizer/core';

let llmService = createLLMService(/* … */);

function setupIPC() {
  // Request-response handler
  ipcMain.handle('llm-testConnection', async (event, provider) => {
    try {
      await llmService.testConnection(provider);
      return { success: true };
    } catch (e) {
      return { success: false, error: (e as Error).message };
    }
  });

  // Add more handlers here …
}

```

Source: [main.js – IPC setup](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/desktop/main.js#L42-L55)

### Exposing the API in the Preload Script

The preload script creates the bridge between main and renderer:

```typescript
// packages/desktop/preload.js
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
  llm: {
    testConnection: (provider) =>
      ipcRenderer.invoke('llm-testConnection', provider),
    // other methods …
  },
  // Optional event helpers
  on: (ev, cb) => ipcRenderer.on(ev, cb),
  off: (ev, cb) => ipcRenderer.off(ev, cb),
});

```

Source: [preload.js – API exposure](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/desktop/preload.js#L64-L73)

### Calling Services from Vue Components

Renderer-side usage inside the Vue UI:

```typescript
// Somewhere in a Vue setup / composition function
export function useLLM() {
  const check = async (provider) => {
    const result = await window.electronAPI!.llm.testConnection(provider);
    if (!result.success) {
      throw new Error(result.error);
    }
    return result;
  };

  return { check };
}

```

The type declaration that makes `window.electronAPI` visible lives in [`packages/ui/src/types/electron.d.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/types/electron.d.ts), which defines the `Window.electronAPI` interface for full method signatures.  
Source: [Electron type definitions](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/ui/src/types/electron.d.ts#L14-L53)

### Handling Server-Push Events

For one-way notifications from main to renderer:

```typescript
// Renderer
if (window.electronAPI?.on) {
  const cleanup = window.electronAPI.on('update-available-info', (info) => {
    console.log('Update available:', info);
  });

  // later …
  // cleanup(); // to remove the listener
}

```

Source: [Electron API best-practice – event handling](https://github.com/linshenkx/prompt-optimizer/blob/develop/docs/guides/electron-api-best-practices.md#L7-L27)

## Key Files and References

| File | Role | Link |
|------|------|------|
| [`packages/desktop/main.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/main.js) | Main-process entry; registers `ipcMain.handle` listeners and creates core service instances. | [View source](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/desktop/main.js) |
| [`packages/desktop/preload.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/preload.js) | Bridge script executed in a sandbox; exposes a safe `electronAPI` via `contextBridge`. | [View source](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/desktop/preload.js) |
| [`packages/ui/src/types/electron.d.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/types/electron.d.ts) | Global TypeScript declarations for the renderer-side `window.electronAPI`. | [View source](https://github.com/linshenkx/prompt-optimizer/blob/develop/packages/ui/src/types/electron.d.ts) |
| [`docs/developer/desktop-developer-guide.md`](https://github.com/linshenkx/prompt-optimizer/blob/main/docs/developer/desktop-developer-guide.md) | High-level architecture description, including the IPC data flow diagram. | [View source](https://github.com/linshenkx/prompt-optimizer/blob/develop/docs/developer/desktop-developer-guide.md#L41-L50) |
| [`docs/guides/electron-api-best-practices.md`](https://github.com/linshenkx/prompt-optimizer/blob/main/docs/guides/electron-api-best-practices.md) | Recommended patterns for invoking IPC and handling results. | [View source](https://github.com/linshenkx/prompt-optimizer/blob/develop/docs/guides/electron-api-best-practices.md#L7-L27) |

These files together illustrate the complete IPC pathway that lets the Electron renderer (the Vue UI) safely call into the Node-based core services without duplicating state or breaking the process isolation model.

## Summary

- **Service-proxy architecture**: The main process hosts the real service instances (`LLMService`, `ModelManager`), while the renderer uses lightweight proxies that forward calls via IPC.
- **Secure bridge**: The [`preload.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/preload.js) script uses `contextBridge.exposeInMainWorld` to create a typed `electronAPI` object, preventing direct Node.js access in the renderer.
- **Request-response pattern**: `ipcRenderer.invoke` and `ipcMain.handle` provide promise-based communication that appears synchronous with `async/await`, with automatic JSON serialization.
- **Single source of truth**: Proxy objects ensure exactly one instance of core services runs in the main process, avoiding state duplication and enabling Node-only APIs like file system access.
- **Type safety**: Global TypeScript declarations in [`packages/ui/src/types/electron.d.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/types/electron.d.ts) provide compile-time checking and IDE autocomplete for all IPC methods.

## Frequently Asked Questions

### What IPC pattern does Prompt Optimizer use?

Prompt Optimizer implements a **high-level service-proxy** pattern. Instead of exposing low-level Electron APIs directly, the main process registers handlers with `ipcMain.handle` that wrap core business logic, while the renderer calls these through typed proxy objects exposed via the preload script. This pattern abstracts the IPC layer so that UI code appears to call local asynchronous functions.

### How does the preload script improve security?

The preload script ([`packages/desktop/preload.js`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/desktop/preload.js)) runs in an isolated context with limited Node.js privileges. It uses `contextBridge.exposeInMainWorld` to selectively expose only the intended API surface (`electronAPI`) to the renderer. This prevents the Chromium-based renderer from directly accessing `require`, `fs`, or other privileged modules, effectively eliminating common Electron security vulnerabilities like remote code execution via XSS.

### Can the renderer process directly access Node.js APIs?

No. The renderer process in Prompt Optimizer runs with **context isolation** enabled and cannot directly import Node.js modules or use native APIs such as the file system or network requests. All privileged operations must go through the IPC bridge: the renderer calls `window.electronAPI` methods, which route through the preload script to the main process where the actual Node.js execution occurs.

### How are errors handled across process boundaries?

IPC handlers in the main process wrap service calls in `try/catch` blocks and return structured result objects rather than throwing raw exceptions. For example, handlers return `{ success: boolean, error?: string }` payloads. This ensures that errors are serialized safely across the IPC boundary without crashing the renderer or main process, and allows the Vue UI to handle failures gracefully using standard promise rejection patterns.