VOLT

Architecture

How VOLT's client, server, cluster daemons and native layer fit together, and how one set of programs runs self-hosted, in a browser tab and in the desktop app.

VOLT is four kinds of programs:

  • The client (packages/apps/client, volt-webapp): the React web app, with a three.js canvas, that people use. It talks to one server over HTTP and Socket.IO.
  • The server (packages/infra/server, volt-server): an Express + Socket.IO API. It owns accounts, teams, permissions, the metadata of every trajectory and analysis, and the real-time fan-out to clients. It holds no simulation data.
  • The cluster daemon (packages/infra/cluster, cluster-daemon): one per machine a team enrolls (a cluster). It stores the team's data on that machine and does the computing: parsing trajectories, rendering models and previews, running plugins, hosting notebook containers.
  • Plugins and native libraries: the analysis programs the daemon runs (C++ executables built on CoreToolkit, or Python projects), and the Node addons the daemon loads to read dumps, build GLB models and rasterize previews.

The split keeps data and compute on machines the team controls. The daemon dials out to the server, so a cluster needs no inbound port; the server coordinates and relays.

Components

The pieces, and where each is documented:

One set of programs, three ways to run

The server and the daemon are the same programs wherever VOLT runs. What changes is the host underneath them.

Self-hostedBrowser tabDesktop app
Where the server and daemon runNode processes, normally the containers of packages/apps/desktop/stack/compose.yml (volt-postgres, volt-server, volt-client, cluster-daemon), plus a daemon on every enrolled machineModule workers of the page, on @volt/web-runtimeThe same workers, in the Electron window that serves the client at https://volt.invalid
Server databasePostgres (DATABASE_URL=postgres://...)SQLite (sqlite-wasm) in the origin private file system (OPFS)Same as the browser
Daemon databasePostgres (volt-cluster), or SQLite when DATABASE_URL is unsetSQLite in OPFSSame as the browser
Object storeDAEMON_DATA_DIR/object-store on the machine's diskOPFSOPFS of the app's persistent session
PluginsNative builds for the machine's platform (linux-x86_64, darwin-arm64, windows-x86_64)wasm-wasm32 builds, run as Node programs in workersSame as the browser
Notebook kernelsJupyter containers on a cluster with DockerPython (Pyodide) processes in the pageSame as the browser
DEPLOYMENT_MODEcloud when deployed with the deploy CLI; the compose file defaults to locallocallocal

A browser runs its own stack when local-stack mode is on (the client stores volt:backend:local-stack in localStorage); the desktop app always does, since its preload exposes window.volt. Only one tab of an origin runs the stack. See Run in the browser, Desktop app and Self-hosting for how to start each.

Networking

Every connection, from the side that opens it:

FromToProtocolWhat travels
BrowserClient hostHTTP(S)The static client bundle. The volt-client image is nginx, which also proxies /api and /socket.io to volt-server on the same origin.
ClientServerHTTPS, Socket.IO (long-polling, upgraded to WebSocket)REST API under /api, live events, uploads and downloads under /api/teams/:teamId/cluster-objects/:token.
Daemon (plane processes)ServerSocket.IO over WebSocket, outbound onlyFour connections, one per channel: control (commands both ways), heartbeat, events, object-gateway (tunneled object bytes).
Daemon (heartbeat plane)ServerHTTP HEAD /healthzLatency probe reported with each heartbeat.
DaemonServerHTTP, /internal/team-cluster/object-store/v1/...Reads and writes of objects owned by another cluster, authenticated with the cluster id and daemon password headers.
ServerDaemon object gatewayHTTP, /internal/object-gateway/v1/buckets/:bucket/objectsObject bytes, when the daemon advertises OBJECT_GATEWAY_PUBLIC_BASE_URL and the server can reach it. Otherwise the same requests tunnel over the object-gateway channel.
ServerServer databaseTCP (Postgres) or file (SQLite)Metadata, key-value state, and LISTEN/NOTIFY for the event bus on Postgres.
DaemonDaemon databaseTCP (Postgres) or file (SQLite)Job queues, daemon state, analysis listing rows.
ServerPlugin registry (REGISTRY_URL)HTTPSPackage search and tarball resolution.
DaemonPlugin registryHTTPSThe plugin tarball the server resolved, verified against its SHA-256.
DaemonDockerUnix socket /var/run/docker.sockJupyter containers, and the container scan that feeds the exposure registry.
ServerJupyter containerHTTP and WebSocket, relayed over the daemon's control channelNotebook UI and kernels under /api/jupyter.

In the browser and the desktop app, the same connections run over the web runtime's loopback network between workers, and the page's service worker relays the client's /api and /socket.io requests to the server worker.

Deployment modes

DEPLOYMENT_MODE is read by the server and exposed at GET /api/system/config as { "mode": "local" | "cloud" }. Any value other than local means cloud.

localcloud
Who it is forOne person on one machine: the browser runtime, the desktop app, a bare compose stackA shared server with many accounts and teams
Sign-inPOST /api/auth/sessions/local signs in as the provisioned local account (local@volt.local) without a password; the client does it automaticallyEmail and password or OAuth. The local sign-in route answers 404.
Client navigationHides the multi-tenant pages (Authentication, Sessions, My Team, Manage Roles)Shows everything

The local account, its team and its cluster are created by @volt/local-stack (packages/shared/local-stack) the first time a local stack starts. The self-hosting CLI reuses the same bootstrap for the stack it deploys.

Lifecycle of a trajectory upload

Uploads go through the server to the trajectory's storage cluster in parts, and the daemon on that cluster does all of the parsing.

Part size is TRAJECTORY_UPLOAD_CHUNK_SIZE, and the signed URLs are valid for TRAJECTORY_UPLOAD_SESSION_TTL_SECONDS (6 hours by default). The steps on the daemon are described in Trajectory processing.

Lifecycle of an analysis

Every analysis the server starts, from the canvas, the SDKs or VOLT AI, is a stage of a pipeline run. The compute cluster that runs it can differ from the storage cluster that holds the trajectory.

A plugin stage whose hash matches an earlier analysis is a cache hit: it creates no analysis and points at the one that already computed it. See Pipelines & analyses for the user's view, and Plugin execution for what the daemon does with a stage.

On this page