How Motrix Architecture Separates Electron UI from aria2 via WebSocket RPC
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, 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.
// 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 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 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 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.
// 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 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.
// 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 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 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
WebSocketTransportto establish a JSON-RPC channel. Aria2RpcClientinsrc/core/engine/aria2/aria2-rpc-client.tshandles authentication and method mapping for all aria2 operations.- The renderer uses
HttpWsTransportto communicate via HTTP and WebSocket through the bridge server, never connecting directly to aria2. src/main/bridge/web-socket-bridge-server.tsroutes 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. 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. 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.
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 →