# How Magnitude Implements an RPC Server in Electron Using IPC and Effect Services

> Discover how Magnitude builds a type-safe RPC server in Electron with Effect RPC and IPC. Learn about bidirectional communication between renderer and main processes.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: internals
- Published: 2026-09-06

---

**Magnitude's desktop application uses Effect RPC layered on top of Electron's IPC channels to provide type-safe, bidirectional communication between the renderer process and the privileged main process.**

The Magnitude desktop client isolates UI code in an Electron renderer while delegating OS-level operations to the main process. This architecture demands a robust remote procedure call mechanism that preserves type safety across process boundaries. The solution combines Electron's `ipcMain`/`ipcRenderer` APIs with Effect-TS's RPC system, creating a clean separation between transport concerns and business logic.

## Core Channel Architecture

All IPC traffic flows through two well-defined channels declared in [[`desktop/src/desktop-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/desktop-rpc.ts)](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/desktop-rpc.ts#L22-L25):

```typescript
const DesktopRpcChannel = {
  request: '__magnitude:desktop-rpc:request',
  response: '__magnitude:desktop-rpc:response',
} as const

```

These strings are deliberately prefixed with `__magnitude` to avoid collisions with standard Electron channels. The **request** channel carries calls from renderer to main; the **response** channel returns encoded results.

## Defining the RPC Contract

The `DesktopRpcs` group (lines 42-159 of [`desktop-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop-rpc.ts)) enumerates every desktop capability as an `Rpc.make` entry. Each definition specifies:

- **Payload schema** – input validation via Effect Schema
- **Success schema** – output type guarantee
- **Streaming flag** – `stream: true` for long-lived operations

```typescript
// Simplified excerpt showing both regular and streaming RPCs
const DesktopRpcs = Rpc.make({
  StorageGetItem: {
    payload: Schema.Struct({ key: Schema.String }),
    success: Schema.NullOr(Schema.String),
  },
  ServiceStart: {
    payload: ServiceStartPayload,
    success: ServiceEvent,
    stream: true, // Returns progress events over time
  },
  BrowserObserve: {
    payload: BrowserObservePayload,
    success: BrowserState,
    stream: true,
  },
})

```

This declarative approach lets the TypeScript compiler enforce contract compliance on both sides of the IPC boundary.

## Server Layer: IPC to Effect Bridge

The main process implements `makeElectronRpcServerLayer` in [[`desktop/src/electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts)](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts#L43-L108). This layer transforms Electron IPC events into Effect RPC protocol messages.

Key responsibilities:

- Registers an `ipcMain` listener on `DesktopRpcChannel.request`
- Extracts `clientId` from `WebContents.id` to track multiple renderer windows
- Forwards encoded requests via `RpcServer.Protocol.writeRequest`
- Sends responses through `webContents.send(DesktopRpcChannel.response, response)`
- Exposes disconnect events and active client tracking through a mailbox interface

The server layer remains transport-agnostic from the Effect perspective—it receives and emits byte arrays while Electron handles the actual IPC mechanism.

## Client Layer: Renderer-Side Transport

The companion `makeElectronRpcClientLayer` (lines 14-41) runs in the renderer process:

```typescript
const clientLayer = Layer.scoped(
  RpcClient.Protocol,
  Effect.gen(function* () {
    const queue = yield* Queue.unbounded()
    
    ipcRenderer.on(DesktopRpcChannel.response, (_, encoded) => {
      Queue.unsafeOffer(queue, encoded)
    })
    
    return {
      writeRequest: (request) => 
        Effect.sync(() => ipcRenderer.send(DesktopRpcChannel.request, request)),
      readResponse: queue.take,
    }
  })
)

```

This symmetry—`writeRequest` to emit, `readResponse` to consume—mirrors the server structure while inverting the direction of control.

## Preload Bridge: Exposing the API

The critical integration point is [[`desktop/src/preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/preload.ts)](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/preload.ts). This script runs with `contextIsolation: true` and constructs:

1. **Managed runtime** – Composes `makeElectronRpcClientLayer` with `RpcClient.make(DesktopRpcs)`
2. **Type-safe facade** – `makeDesktopApi` returns a `DesktopApi` object mapping each RPC to a callable method
3. **Global exposure** – `contextBridge.exposeInMainWorld("__magnitudeDesktop", api)` (line 87)

The resulting `window.__magnitudeDesktop` object presents synchronous-looking methods that internally marshal through Effect:

```typescript
// From the renderer's perspective: a normal async function
const token = await window.__magnitudeDesktop.storage.getItem('authToken')

// Streaming operations receive callbacks for each event
const stop = window.__magnitudeDesktop.serviceStarter.start(
  (event) => console.log('Progress:', event),
  (err) => console.error('Failed:', err),
  () => console.log('Complete')
)

```

## Effect Services in Practice

Concrete desktop capabilities—dialogs, notifications, browser automation, file storage—are all implemented as **Effect RPC calls** following the `DesktopRpcs` contract. The renderer never imports Node modules directly; instead, it invokes methods through the preloaded `DesktopApi`.

Example patterns from the codebase:

```typescript
// File dialog (one-shot request/response)
async function openFile() {
  const paths = await window.__magnitudeDesktop.dialogs.openFile({ 
    multiple: true 
  })
  return paths
}

// Browser observation (streaming state updates)
const stopObservation = window.__magnitudeDesktop.browser.observe(
  (state) => updateUI(state),
  (err) => handleError(err),
  () => console.log('Observation ended')
)

// Cleanup when component unmounts
return () => stopObservation()

```

## Bootstrapping the Application

The main process entry point ([[`desktop/src/main.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/main.ts)](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/main.ts)) installs the RPC server layer during application startup. This ensures the IPC listener is active before any renderer windows attempt to connect. Each new `BrowserWindow` automatically receives the preloaded API via [`preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/preload.ts) injection.

## Summary

- **Channel definition** in [`desktop-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop-rpc.ts) establishes two IPC constants for bidirectional traffic
- **RPC group** enumerates all desktop operations with schemas and streaming flags
- **Server layer** ([`electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/electron-rpc.ts)) adapts Effect RPC protocol to `ipcMain`/`webContents.send`
- **Client layer** mirrors this adaptation for `ipcRenderer` in the preload context
- **Preload bridge** composes the runtime and exposes `window.__magnitudeDesktop` via `contextBridge`
- **Type safety** is preserved end-to-end through Effect Schema and the generated `DesktopApi`

## Frequently Asked Questions

### How does Magnitude maintain type safety across the Electron IPC boundary?

The `DesktopRpcs` group in [`desktop-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop-rpc.ts) defines every operation with Effect Schema. Both client and server derive their TypeScript types from these schemas. The preload script uses `RpcClient.make(DesktopRpcs)` to generate a typed client, while the server validates incoming payloads against the same schemas before execution.

### Why use Effect RPC instead of raw Electron IPC?

Raw IPC requires manual serialization, handler registration, and error propagation. Effect RPC provides structured request/response correlation, streaming support through the `stream: true` flag, and integration with Effect's error handling and dependency injection. The [`electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/electron-rpc.ts) layer isolates Electron-specific code to approximately 150 lines while the business logic remains pure Effect services.

### Can multiple renderer windows use the RPC server simultaneously?

Yes. The server layer extracts `clientId` from `WebContents.id` for each incoming request and tracks active clients through a mailbox interface. Responses are routed back to the specific `webContents` instance that originated each call, preventing cross-window interference.

### How are streaming operations like `BrowserObserve` implemented?

Streaming RPCs set `stream: true` in their definition. The server emits multiple responses over the same channel, each encoded and sent via `webContents.send`. The client layer queues these responses and delivers them through the callback interface exposed by `makeDesktopApi`, allowing the renderer to receive incremental updates without polling.