# Embedded Browser Architecture in Magnitude: BrowserWindow and Preload Scripts Explained

> Understand Magnitude's embedded browser architecture. Learn how BrowserWindow and preload scripts securely connect your UI to Node.js capabilities for robust applications.

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

---

**Magnitude's Electron renderer uses a hardened `BrowserWindow` with context isolation and a strictly controlled preload script to safely bridge the web-based UI to native Node.js capabilities.**

Magnitude is an open-source testing framework for AI agents, and its desktop application embeds a Chromium-based browser using Electron. The **embedded browser architecture** centers on two core components: the `BrowserWindow` instance created in the main process and a **preload script** that exposes a minimal, typed RPC interface to the renderer. This design follows Electron's security best practices while enabling the Magnitude SDK to power the UI.

## BrowserWindow Configuration in the Main Process

The main process instantiates the embedded browser in [`desktop/src/electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts). This file handles window creation, IPC registration, and the secure loading of the Magnitude web client.

### Security-First Window Options

The `BrowserWindow` constructor explicitly disables dangerous defaults:

```typescript
// desktop/src/electron-rpc.ts (architectural pattern)
const win = new BrowserWindow({
  width: 1024,
  height: 800,
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),   // isolated preload entry
    contextIsolation: true,                         // enforces context separation
    nodeIntegration: false,                         // denies direct Node access
    sandbox: true,                                  // enables Chromium sandbox
  },
});
win.loadURL('app://./index.html');

```

- **`contextIsolation: true`** — Forces the preload script to run in an isolated JavaScript world, preventing prototype pollution attacks from the web content.
- **`nodeIntegration: false`** — Ensures the renderer cannot access `require()` or Node.js APIs.
- **`sandbox: true`** — Applies OS-level process sandboxing to the renderer.

The `app://` protocol is registered by the main process to serve bundled assets without exposing the local filesystem directly.

## Preload Script Architecture

The preload script at [`desktop/preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/preload.ts) (compiled to [`preload.js`](https://github.com/magnitudedev/magnitude/blob/main/preload.js) by Vite) is the sole communication channel between the Magnitude UI and the Electron main process.

### Exposed API Surface

Using `contextBridge`, the preload exposes a single `window.magnitude` object with strictly typed methods:

```typescript
// desktop/preload.ts (excerpt)
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('magnitude', {
  invoke: (channel: string, ...args: any[]) => 
    ipcRenderer.invoke(channel, ...args),
  on: (channel: string, listener: (...args: any[]) => void) =>
    ipcRenderer.on(channel, (_event, ...args) => listener(...args)),
  send: (channel: string, ...args: any[]) =>
    ipcRenderer.send(channel, ...args),
});

```

No other globals are exposed. The UI must route all native operations through these three methods.

### Type Safety and Validation

The preload does not perform business logic. Instead, it forwards serialized messages that are validated against **Effect-TS schemas** on both sides:

- Outbound: The SDK in [`packages/sdk/src/rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/rpc.ts) defines RPC contracts as Effect schemas.
- Inbound: The main process unwraps effects and returns serialized results.

This preserves type safety across the process boundary without exposing schema definitions to the renderer.

## RPC Flow: From UI to SDK

The **embedded browser architecture** implements a four-stage request lifecycle:

1. **UI Invocation** — Renderer code calls `window.magnitude.invoke('agent.getStatus', payload)`.
2. **IPC Forwarding** — Preload forwards to `ipcRenderer.invoke`, which serializes across the process boundary.
3. **Main Handling** — [`desktop/src/electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts) receives the channel, executes the corresponding SDK effect from [`packages/sdk/src/rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/rpc.ts), and awaits resolution.
4. **Response Return** — Result serializes back through `ipcMain.handle`, resolves in the preload, and returns to the UI as a Promise.

```typescript
// Main process handler registration (desktop/src/electron-rpc.ts pattern)
ipcMain.handle('agent.getStatus', async (_event, params) => {
  const effect = agentRpc.getStatus(params);  // Effect-TS effect
  return await Effect.runPromise(effect);      // executes in main process
});

```

```typescript
// UI consumption (renderer context)
async function checkStatus() {
  const status = await window.magnitude.invoke('agent.getStatus');
  // status is typed via Effect-TS schema inference
}

```

## Build and Bundling Configuration

The Vite configuration at [`desktop/electron.vite.config.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/electron.vite.config.ts) produces distinct build targets for the preload and renderer:

| Target | Entry | Output | Build Options |
|--------|-------|--------|---------------|
| Preload | [`desktop/preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/preload.ts) | [`preload.js`](https://github.com/magnitudedev/magnitude/blob/main/preload.js) | `target: 'node'`, `format: 'cjs'`, no minification of IPC channels |
| Renderer | [`desktop/src/main.tsx`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/main.tsx) | [`index.html`](https://github.com/magnitudedev/magnitude/blob/main/index.html) + assets | Standard web build, imports from `client-common` and `sdk` |

This separation prevents the large UI bundle from bloating the preload script, keeping the isolated context lightweight and inspectable.

## Key Source Files

- [`desktop/src/electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts) — `BrowserWindow` creation and IPC handler registration
- [`desktop/preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/preload.ts) — Isolated preload script exposing `window.magnitude`
- [`desktop/electron.vite.config.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/electron.vite.config.ts) — Dual-target Vite configuration
- [`packages/sdk/src/rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/rpc.ts) — SDK RPC definitions and Effect-TS schemas
- [`packages/client-common/src/effect-query.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/client-common/src/effect-query.ts) — Client-side query layer calling `window.magnitude.invoke`

## Summary

- **BrowserWindow** in [`desktop/src/electron-rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/src/electron-rpc.ts) creates a sandboxed, context-isolated renderer with no direct Node access.
- **Preload script** in [`desktop/preload.ts`](https://github.com/magnitudedev/magnitude/blob/main/desktop/preload.ts) exposes only three IPC methods via `contextBridge`, forming a minimal attack surface.
- **Effect-TS schemas** in [`packages/sdk/src/rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/rpc.ts) enforce type safety across the process boundary without exposing types to the renderer.
- **Vite dual build** separates preload and renderer bundles for optimal loading and security auditing.

## Frequently Asked Questions

### Why does Magnitude disable `nodeIntegration` in the BrowserWindow?

Disabling `nodeIntegration` prevents untrusted web content from accessing Node.js APIs like `fs` or `child_process`. Magnitude's UI renders user-provided test configurations and agent outputs, so this hardening is essential. All native operations flow through the typed `window.magnitude` bridge instead.

### How does the preload script maintain type safety without TypeScript in the renderer?

The preload exposes untyped JavaScript methods, but the **Effect-TS schemas** in [`packages/sdk/src/rpc.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/rpc.ts) validate every argument and return value. The `client-common` package wraps the raw `window.magnitude.invoke` calls with generated TypeScript clients, so developers get full autocomplete and compile-time checking without exposing schema code to the isolated renderer.

### What is the `app://` protocol used for?

The `app://` protocol serves the bundled web assets (HTML, JS, CSS) through a custom handler registered in the main process. This avoids `file://` protocol limitations and security restrictions while keeping assets offline. The protocol is registered before the `BrowserWindow` loads its URL.

### Can the preload script be modified at runtime?

No. The preload script is loaded from a bundled file ([`preload.js`](https://github.com/magnitudedev/magnitude/blob/main/preload.js)) whose path is resolved at startup via `path.join(__dirname, 'preload.js')`. In packaged builds, this file resides in an ASAR archive. `contextIsolation` further prevents the renderer from modifying or replacing the exposed `window.magnitude` object after creation.