# How Motrix Architecture Separates Electron UI from aria2 via WebSocket RPC

> Discover how Motrix architecture uses WebSocket RPC to separate Electron UI from aria2. Learn how this separation ensures a sandboxed UI and real-time daemon communication.

- Repository: [Dr_rOot/Motrix](https://github.com/agalwood/Motrix)
- Tags: architecture
- Published: 2026-08-19

---

**Motrix isolates the aria2 download engine in the main process and bridges it to the Electron renderer through a WebSocket RPC layer, ensuring the UI remains sandboxed while maintaining real-time bidirectional communication with the download daemon.**

Motrix is a full-featured download manager built on Electron that leverages the high-performance aria2c binary for BitTorrent, HTTP, and FTP downloads. Rather than embedding download logic directly in the renderer, the application implements a strict architectural boundary where the UI process never touches the aria2 binary directly. Instead, a layered RPC architecture over WebSocket and HTTP transports mediates all communication between the Electron frontend and the download engine.

## WebSocket RPC Architecture Layers

The separation relies on six distinct layers that abstract binary execution, wire protocol details, and UI compatibility concerns.

### Engine Process and Transport Initialization

In [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts), the main process spawns the aria2 daemon and establishes the primary communication channel. The `WebSocketTransport` class wraps the Node.js `ws` library to create a persistent connection to aria2's JSON-RPC endpoint.

```typescript
// src/main/index.ts
import { WebSocketTransport } from '@core/engine/aria2/web-socket-transport';
import { JsonRpcProtocol } from '@core/engine/aria2/json-rpc-protocol';
import { Aria2RpcClient } from '@core/engine/aria2/aria2-rpc-client';

const transport = new WebSocketTransport();
const protocol = new JsonRpcProtocol(transport);
const rpcClient = new Aria2RpcClient(transport, protocol, secret);

// Connect to aria2 daemon once EngineSupervisor reports the listening port
await rpcClient.connect(port);

```

The `WebSocketTransport` implementation in [`src/core/engine/aria2/web-socket-transport.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/engine/aria2/web-socket-transport.ts) handles connection state management, reconnection logic, and message framing. It exposes `connect()`, `send()`, and `onMessage()` methods that isolate the protocol layer from socket mechanics.

### JSON-RPC Protocol Abstraction

[`src/core/engine/aria2/json-rpc-protocol.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/engine/aria2/json-rpc-protocol.ts) implements the `JsonRpcProtocol` class, which marshals method calls into JSON-RPC 2.0 format and unmarshals responses. This layer handles request batching, error code translation, and ID correlation for asynchronous responses.

### RPC Client with Secret Authentication

The `Aria2RpcClient` class defined in [`src/core/engine/aria2/aria2-rpc-client.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/engine/aria2/aria2-rpc-client.ts) provides type-safe methods for every aria2 RPC action, including `addUri`, `tellStatus`, and `remove`. It automatically injects the secret token via the `withSecret()` helper to satisfy aria2's authentication requirements.

```typescript
// src/core/engine/aria2/aria2-rpc-client.ts
public async addUri(uris: string[], options?: object): Promise<string> {
  return this.call('aria2.addUri', [this.withSecret(uris, options)]);
}

private withSecret(...params: any[]): any[] {
  return [`token:${this.secret}`, ...params];
}

```

This client also subscribes to aria2 push notifications such as `aria2.onDownloadComplete`, translating WebSocket messages into typed JavaScript events.

### Renderer-Side HTTP Transport

The renderer process uses `HttpWsTransport` from [`src/renderer/lib/transport/http-ws.ts`](https://github.com/agalwood/Motrix/blob/main/src/renderer/lib/transport/http-ws.ts) to maintain browser-compatible sandboxing. Rather than opening raw WebSocket connections to the aria2 port, the UI sends HTTP POST requests to local bridge endpoints.

```typescript
// src/renderer/lib/transport/http-ws.ts
import { HttpWsTransport } from '@renderer/lib/transport/http-ws';

const transport = new HttpWsTransport('http://localhost:3000');

// Invoke RPC method via HTTP bridge
const gid = await transport.invoke('command:addUri', 
  ['https://example.com/file.bin'], 
  { dir: '/downloads' }
);

```

For real-time events, the transport establishes a secondary WebSocket connection to `/rpc/events`, allowing the UI to receive download progress and completion notifications without violating Content Security Policy restrictions.

### Bridge Server Routing

[`src/main/bridge/web-socket-bridge-server.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/bridge/web-socket-bridge-server.ts) implements the `WebSocketBridgeServer` class, which exposes `/rpc/command/*` and `/rpc/query/*` HTTP endpoints alongside the `/rpc/events` WebSocket. It routes incoming renderer requests to the `Aria2RpcClient` and multiplexes aria2 notifications to all connected UI clients.

### High-Level Adapter Interface

[`src/core/engine/aria2/aria2-adapter.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/engine/aria2/aria2-adapter.ts) provides the `Aria2Adapter` class used throughout the application. This adapter abstracts RPC method names and parameter structures, presenting a clean domain API for task creation, pause/resume operations, and status polling.

## Security and Process Isolation Benefits

The WebSocket RPC architecture enforces security through multiple mechanisms.

**Secret Token Injection:** The `Aria2RpcClient` appends the authentication token to every RPC call, ensuring that even if the renderer were compromised, it could not construct valid aria2 commands without traversing the bridge.

**Process Independence:** Because the UI talks exclusively to the HTTP bridge, the aria2 daemon can restart, crash, or upgrade without requiring a renderer reload. The `WebSocketTransport` implements automatic reconnection logic that restores the connection when the daemon becomes available again.

**Sandbox Compatibility:** The renderer's use of standard `fetch()` and browser WebSocket APIs means the UI code could theoretically run in a standard browser context, not just Electron's Node.js-enabled renderer.

## Summary

- Motrix spawns the aria2 binary in the main process and connects via `WebSocketTransport` to establish a JSON-RPC channel.
- `Aria2RpcClient` in [`src/core/engine/aria2/aria2-rpc-client.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/engine/aria2/aria2-rpc-client.ts) handles authentication and method mapping for all aria2 operations.
- The renderer uses `HttpWsTransport` to communicate via HTTP and WebSocket through the bridge server, never connecting directly to aria2.
- [`src/main/bridge/web-socket-bridge-server.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/bridge/web-socket-bridge-server.ts) routes requests between the sandboxed UI and the download engine.
- This separation allows independent process lifecycles, secure token management, and browser-compatible renderer code.

## Frequently Asked Questions

### How does the renderer receive real-time download progress without direct WebSocket access to aria2?

The renderer opens a WebSocket connection to `/rpc/events` on the local bridge server implemented in [`src/main/bridge/web-socket-bridge-server.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/bridge/web-socket-bridge-server.ts). The bridge subscribes to aria2 notifications via the main process's `WebSocketTransport` and forwards these events to all connected renderer clients. This maintains the security boundary while supporting live progress updates.

### What happens to active downloads if the Electron UI crashes or reloads?

Because the aria2 daemon runs as a separate process managed by `EngineSupervisor` in the main process, downloads continue uninterrupted when the renderer reloads. The bridge server maintains the WebSocket connection to aria2 independently, and the new renderer instance can reconnect to `/rpc/events` to resume monitoring existing tasks through their GIDs.

### Why does Motrix use HTTP for RPC commands instead of WebSocket in the renderer?

HTTP requests via `fetch()` provide better compatibility with Electron's context isolation and Content Security Policy settings. The `HttpWsTransport` uses HTTP POST for command invocation to avoid maintaining persistent WebSocket connections for request-response patterns, reserving WebSocket connections for the event stream where real-time push is actually required.

### Where is the authentication secret stored to prevent exposure in the renderer?

The aria2 RPC secret is injected by the main process when constructing `Aria2RpcClient` in [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts). The secret never reaches the renderer; instead, the bridge server validates renderer requests at the HTTP boundary before forwarding them to the authenticated RPC client running in the main process.