How Magnitude Implements an RPC Server in Electron Using IPC and Effect Services
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#L22-L25):
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) 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: truefor long-lived operations
// 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#L43-L108). This layer transforms Electron IPC events into Effect RPC protocol messages.
Key responsibilities:
- Registers an
ipcMainlistener onDesktopRpcChannel.request - Extracts
clientIdfromWebContents.idto 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:
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). This script runs with contextIsolation: true and constructs:
- Managed runtime – Composes
makeElectronRpcClientLayerwithRpcClient.make(DesktopRpcs) - Type-safe facade –
makeDesktopApireturns aDesktopApiobject mapping each RPC to a callable method - Global exposure –
contextBridge.exposeInMainWorld("__magnitudeDesktop", api)(line 87)
The resulting window.__magnitudeDesktop object presents synchronous-looking methods that internally marshal through Effect:
// 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:
// 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)) 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 injection.
Summary
- Channel definition in
desktop-rpc.tsestablishes two IPC constants for bidirectional traffic - RPC group enumerates all desktop operations with schemas and streaming flags
- Server layer (
electron-rpc.ts) adapts Effect RPC protocol toipcMain/webContents.send - Client layer mirrors this adaptation for
ipcRendererin the preload context - Preload bridge composes the runtime and exposes
window.__magnitudeDesktopviacontextBridge - 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 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →