# How PI-Desktop Handles IPC Communication Between Renderer and Host Processes

> Learn how PI-Desktop manages IPC communication. Discover its typed, versioned system using contextBridge, allow-lists, and IpcRegistrar for seamless renderer and host process interaction.

- Repository: [Lan/PI-Desktop](https://github.com/vastsa/PI-Desktop)
- Tags: how-to-guide
- Published: 2026-09-12

---

**PI-Desktop implements a typed, versioned IPC system using Electron's `contextBridge` with strict allow-lists, protocol version 11 handshake, and a centralized `IpcRegistrar` that routes all messages between the renderer UI and the Rust/Node host core.**

The `vastsa/PI-Desktop` repository demonstrates a production-grade approach to isolating the Electron renderer process from low-level system operations. By enforcing a contract-driven protocol documented in [`docs/spec/03-runtime/01-ipc-protocol.md`](https://github.com/vastsa/PI-Desktop/blob/main/docs/spec/03-runtime/01-ipc-protocol.md), the application ensures that the UI layer cannot directly access SQLite databases or OS resources, instead delegating all state mutations to the authoritative host processes through a secure bridge.

## The Typed Preload Bridge

PI-Desktop exposes a read-only `IPC` object to the renderer via a preload script located at [`apps/desktop/electron/main/preload.ts`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/preload.ts). This script uses Electron's `contextBridge.exposeInMainWorld` to inject a namespaced API that strictly limits available capabilities.

The bridge exposes two distinct namespaces:

- **`IPC.invoke.<domain>/<action>`** – Request/response style calls for synchronous-like operations
- **`IPC.event.<domain>/event/<name>`** – Publish/subscribe streams for real-time updates

The preload script maintains an explicit allow-list of channels, ensuring that only approved domains and actions can traverse the process boundary. This prevents arbitrary code execution and limits the attack surface if the renderer process is compromised.

```ts
// Renderer side: Invoking a host method through the preload bridge
await window.ipc.invoke(IPC.invoke.session.list);   // Returns SessionSummary[]

```

## Stable Channel Naming and Versioning

All IPC channels follow a strict naming convention defined in the protocol specification:

```

invoke: pi-desktop/<domain>/<action>
event:  pi-desktop/<domain>/event/<name>

```

Concrete examples from the codebase include `pi-desktop/agent/prompt`, `pi-desktop/session/list`, and `pi-desktop/notification/event/changed`. This hierarchical structure allows both the renderer and main process to validate channel existence and enforce domain separation.

At startup, both sides exchange a `protocolVersion` (currently **11**) during the handshake. A version mismatch aborts the connection and prompts the user to upgrade, preventing silent failures due to API drift. This version check is implemented in handlers such as `IPC.invoke.appGetVersion` within [`apps/desktop/electron/main/ipc/app-ipc.ts`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/ipc/app-ipc.ts):

```ts
// Main process: Version and protocol compatibility check
handle(IPC.invoke.appGetVersion, async () => {
  const host = getHost();
  const hostVersion = host
    ? await host.call<{ version: string; protocolVersion: number }>("app.getVersion")
    : undefined;
  return {
    name: APP_NAME,
    version: APP_VERSION,
    protocolVersion: PROTOCOL_VERSION,  // Currently 11
    hostProtocolVersion: hostVersion?.protocolVersion,
    hostVersion: hostVersion?.version,
    platform: process.platform,
    arch: process.arch,
  };
});

```

## Centralized Handler Registration

The main process instantiates an `IpcRegistrar` defined in [`apps/desktop/electron/main/ipc/types.ts`](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/ipc/types.ts) to map concrete handler functions to channel names. Domain-specific modules such as [`app-ipc.ts`](https://github.com/vastsa/PI-Desktop/blob/main/app-ipc.ts) (application lifecycle) and [`agent-ipc.ts`](https://github.com/vastsa/PI-Desktop/blob/main/agent-ipc.ts) (agent runtime) register their handlers using the registrar's `handle()` and `handleWithEvent()` methods.

This centralized approach ensures that all IPC capabilities are declared in one of a few known locations, making security audits straightforward. The registrar also maintains type safety by validating that handlers return the structures expected by the renderer's TypeScript definitions exported from [`packages/shared/src/index.ts`](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/index.ts).

```ts
// Main process: Emitting a host-side event to the renderer
emitAgentEvent({
  sessionId,
  ts: Date.now(),
  event: { 
    type: "planning_state", 
    state: "planning", 
    kind: "plan", 
    proposalId, 
    title, 
    markdown, 
    question 
  },
});

```

## Security Boundaries and Error Handling

PI-Desktop enforces a strict security model where the renderer never accesses persistent storage or native APIs directly. All data-affecting operations—such as session CRUD, plugin management, and secret handling—are routed through the IPC bridge to the **host-core** (Rust) or **agent-runtime** (Node) for authoritative processing.

Every IPC response is wrapped in a `Result<T>` envelope with the structure `{ok: true, data}` or `{ok: false, error}`. Errors use a structured `AppError` type containing a machine-readable code and human-readable message. For long-running tasks, intermediate results stream via the event channel rather than accumulating in memory, preventing oversized responses that could crash the renderer.

Host-side events like `plans.changed` or `notification/event/changed` are forwarded through the shared event channel `IPC.event.<domain>/event/<name>`. The renderer subscribes once at startup and retains subscriptions across hot reloads, guaranteeing UI consistency even during development.

## Summary

- **Typed Preload Bridge**: The [`preload.ts`](https://github.com/vastsa/PI-Desktop/blob/main/preload.ts) script exposes a namespaced `IPC` object via `contextBridge`, enforcing an allow-list of approved channels.
- **Protocol Versioning**: Version 11 handshake ensures both renderer and host speak the same API, preventing compatibility issues.
- **Centralized Registration**: The `IpcRegistrar` in [`types.ts`](https://github.com/vastsa/PI-Desktop/blob/main/types.ts) coordinates handlers across domain-specific files like [`app-ipc.ts`](https://github.com/vastsa/PI-Desktop/blob/main/app-ipc.ts) and [`agent-ipc.ts`](https://github.com/vastsa/PI-Desktop/blob/main/agent-ipc.ts).
- **Security Isolation**: All storage access is restricted to the host process; the renderer communicates exclusively through the IPC bridge.
- **Structured Errors**: All responses use a `Result<T>` envelope with standardized error codes for reliable error handling.

## Frequently Asked Questions

### How does PI-Desktop prevent unauthorized IPC channels from being accessed?

The preload script maintains an explicit allow-list that restricts which channels are exposed to the renderer. Only domains and actions defined in the shared IPC constants ([`packages/shared/src/index.ts`](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/index.ts)) are available via `window.ipc`, preventing the renderer from calling arbitrary main process methods.

### What happens if the renderer and host process have mismatched protocol versions?

During the initialization handshake, both processes exchange their `protocolVersion` (currently 11). If the versions do not match, the connection aborts and the user receives a prompt to upgrade the application. This prevents silent data corruption or API mismatches between the UI and core logic.

### How are events streamed from the host to the renderer without blocking the UI?

Long-running operations emit incremental updates through the `IPC.event.<domain>/event/<name>` channel using the `handleWithEvent` registration method. This publish/subscribe pattern allows the host to stream progress notifications while the renderer remains responsive, avoiding the need for large synchronous payloads.