Apollo Download ManagerApollo Download Manager
On this page

Architecture

Apollo Download Manager is a Tauri 2 desktop app with a React UI and a Rust core. It downloads files over HTTP, HTTPS, FTP and SFTP with its own transfer engine, on libcurl and, for SFTP, russh, and uses libtorrent for BitTorrent. It contains no aria2: the direction is recorded in ADR-0001.

This page describes Apollo Download Manager as it is built. Checked against the code at commit 9a4f484 (2026-10-30, #80). Each component below names the issues that built it; what is still to come is said where it applies.

Components

 Browser extension ──(native messaging, stdio)──▶ Native host ──(local IPC)──▶ ┐
                                                                               │
 React UI (src/) ◀──(typed commands and events)──▶ Desktop shell (src-tauri/) ◀┘
                                                          │
                                                          ▼
                                     velocity-core: manager, queue, policy, validation,
                                     persistence (SQLite), settings, categories, history
                                          │                               │
                            DirectTransfer contract              TorrentEngine contract
                                          │                               │
                                          ▼                               ▼
                  Transfer engine: probe, leases, checkpoints,     Torrent helper (child,
                  integrity, retries, limits (child, ADR-0012)     ADR-0014) → torrent bridge
                                          │                         → libtorrent (v1, v2,
                                          ▼                         hybrid, magnets)
                  libcurl layer: the only code that calls libcurl         │
                                          │                               │
                                          ▼                               ▼
                         HTTP, HTTPS, FTP and SFTP servers         peers and trackers
ComponentPathResponsibilityTodayBuilt in
UIsrc/React and TypeScript. It talks to Rust only through typed commands and events, and validates every native response before useThe window (#29): first-run setup, sidebar with each list's count, status bar, Categories screen, theme, saved layout, and the exit confirmation; checks the contract version (src/contract.ts). The downloads list (#59), the Add download workflow (#60), the details panel (#61), the torrent screens (#63), the history, the Settings screen (#30) and diagnostics (#28). Its structure is in UI_ARCHITECTURE.md#29, #59 to #61, #63
Desktop shellsrc-tauri/Tauri 2 app: thin typed commands that delegate to the core, the window, the tray, notifications, the single instance and the browser bridgeapp_info, and since #58 the downloads: it opens the database, starts the supervised engine from beside its own executable (or runs without one and says why), runs the manager on its own thread, sends downloads-snapshot events and has V1's direct-download commands (CONTRACTS.md). Since #29: the tray, close to tray, start minimised, the quit request, desktop notifications and the folder chooser (CONTRACTS.md)#64, #65
Corecrates/velocity-core/Everything that doesn't need a window: domain models, the download manager and queue, input validation, naming, categories, settings, history and SQLite persistenceapp_info(), the domain types (#25), identity and data paths (#27), the manager's first slice (#36), and the SQLite database with versioned migrations, backups and unknown-value handling (DATA_AND_MIGRATIONS.md, #26). The manager saves its downloads, brings them back when the app starts, retries, removes with the download's own files only, and resumes what a lost engine was running (#58, ENGINE_CONTRACT.md). Categories, the folder a download goes in, and what happens when its name is taken (#31, FILE_NAMING.md)#25, #26, #30 to #32, #50, #58
DirectTransfer contractcrates/velocity-core/ (ADR-0002)The engine-neutral boundary for direct downloads: commands, events, statuses, errors and capabilities. The manager never sees libcurl types. Details: ENGINE_CONTRACT.mdThe trait, its wire commands, a test-only fake engine, and the manager (manager/)#36
Transfer enginecrates/velocity-transfer/ (ADR-0002)The range probe, leased segmentation, durable checkpoints, changed-object detection, integrity, retries, connection budgets and speed limitsThe probe as the first segment, with every response of report section 5.1 handled and classified before any byte is written: ranges, sequential ranges without a total, one stream, and a retry without Range after a multipart answer (#41, ENGINE_INTERNALS.md). The range scheduler (#42): leases that own their bytes alone, an equal share per free connection, splits that stop the old request at the new boundary, invariants I1 to I7 checked by property tests, and a fallback to one stream after a second anomaly. Checkpoints (#43): the part file is synced every 2 s or 16 MiB and on a pause, and the durable coverage is sent to the app. A resume trusts only a checkpoint that fits, pause and resume work, and a crash resumes from the last checkpoint (100 randomised kill -9 trials). A sparse .vdmpart part file on Windows, and a finish that renames without ever replacing an existing file (ENGINE_INTERNALS.md). InProcessEngine implements the contract over it, refusing what it can't do yet as Unsupported; velocity-transfer get runs one download by hand. Connections (#47): set_connections works while a transfer runs, the per-host cap and a shared budget are shared max-min fairly, each download starts with every connection it may use and drops the ones that don't help (#221), and over HTTP/2 it multiplexes first and switches to connections of their own only when measurably faster (ENGINE_INTERNALS.md). Changed-object detection with strong validators and If-Range (#45), whole-file and piece integrity (#46), retries and stalls (#48), speed limits (#49), FTP and FTPS (#51), SFTP (#53), Metalink (#54), mirrors (#55) and the proxy (#192), all in ENGINE_INTERNALS.md. Results: engine-prototype-results.md#37, #40 to #49, #51, #53 to #55, #192
libcurl layercrates/velocity-curl/ (ADR-0002)The only crate that touches libcurl: FFI and the Multi loop on its own thread. libcurl 8.23.0 or later, with a build identity record; obtained as ADR-0004 decidesLinks the Velocity-built libcurl (BUILDING_NATIVE.md); Libcurl::start() refuses a libcurl that fails the capability check. CurlLoop runs every transfer on one Multi thread: a gate sees the final response head before any body byte, a slow reader pauses its transfer instead of growing a buffer, and the protocol and redirect allowlists, an explicitly empty proxy and no Accept-Encoding are set on every fetch#35, #37
SFTP layercrates/velocity-sftp/ (ADR-0013)The only crate that touches russh. SFTP fetches on their own tokio runtime, answering with the same events as libcurl's through the shared Transport interface and a FetchPipe. Apollo DM's own known_hosts file: unknown host keys wait for the user, changed keys are always refusedNo native library: russh with ring. The host key is checked before the login; password and key logins; reads from an offset, so the engine's probe, checkpoints and retries apply as for FTP#53
Engine processvelocity-transfer serve, a child process (ADR-0012)The engine runs as a child process the app starts and supervises, speaking a versioned protocol over stdiovelocity-transfer serve speaks the framed, versioned protocol of ENGINE_PROTOCOL.md. ChildProcessEngine is its client: one argument, a cleared environment, and stderr kept in memory. SupervisedEngine restarts a crashed engine with backoff and a cap, and reports the transfers a crash lost as retryable failures. The same contract tests pass in process and as a child process. The app runs it from beside its own executable (#58), where the Windows installer puts it (PACKAGING.md, #81)#38, #39
Child-process layercrates/velocity-child/ (ADR-0014)What the engine and the torrent helper share: the framing, the handshake and envelope, the serve loop, the app's client, and the supervisor with its restarts and hang checksBoth children run on it; each supplies a contract and a book for its supervisor (ENGINE_PROTOCOL.md)#39, #62
TorrentEngine contractcrates/velocity-core/ (torrent_engine.rs)The boundary for torrents: commands, events and snapshots in Apollo DM's types. Torrents never pass through DirectTransferVersion 2: settings, add a .torrent file or a magnet, file choice, a torrent's metadata in chunks, pause, resume, remove, snapshot (TORRENTS.md). Torrent and magnet parsing in torrent.rs#62
Torrent engine and helpercrates/velocity-torrent/ (ADR-0014)What reaches libtorrent and what its state means; the helper velocity-torrent serve, its client and its supervisorDownloads v1, v2-only and hybrid torrents and magnets, checks every input first (a magnet's metadata included, before any byte is written), downloads only the chosen files, and completes a torrent only after its files are flushed. A crashed helper is restarted. Seeding, trackers and port mapping too (#62). The app starts it from beside its own executable, without linking libtorrent, and the manager runs torrents beside direct downloads (#58, TORRENTS.md)#62
Torrent bridgecrates/velocity-lt/ (ADR-0002)The only crate that calls libtorrent: a small C++ shim over one session. It never deletes fileslibtorrent 2.0.15, built with vcpkg (BUILDING_NATIVE.md)#62
Native hostcrates/velocity-native-hostA short-lived process started by the browser. It validates messages and forwards them to the app over local IPCSince #65: the program velocity-native-host-v2, with the protocol, the local connection and registration in velocity_core::browser, and the app's side in src-tauri/src/browser.rs (BROWSER_EXTENSION.md)#65
Browser extensionbrowser-extension/Offers browser downloads to Apollo DM and falls back to the browser on any failureSince #64: interception, the rules, the popup, settings, the ask window and magnet capture, ported from V1 with Apollo DM's identity (BROWSER_EXTENSION.md)#64

Placement rules (also in AGENTS.md):

  • UI code lives in src/.

  • Tauri commands in src-tauri/ validate input and delegate to the core, without duplicating its logic. This rule is carried over from V1's AGENTS.md.

  • Reusable domain logic lives in crates/. This rule is also carried over from V1.

  • Only the libcurl layer calls libcurl, and only the torrent bridge calls libtorrent. This follows the research report, section 4.2, decided in ADR-0002.

Ownership of state

Each piece of state has one owner. Other components ask the owner instead of keeping their own copy.

OwnerState
Core (the manager)Download records: a stable app id per download, its source, destination, category and status, and its history. Policy: the queue order and concurrency limit, the schedule window, naming and conflict decisions, and settings. Validation: validation of all input. Records are stored in SQLite with versioned migrations (#26)
Transfer engineActive transfers: connections, leases, received bytes and rate. It also owns the durable progress of each transfer: it syncs the part file, then sends a Checkpoint event, and the manager saves it in the database's checkpoints table (PO decision, 2026-10-22, on #26; #43, ENGINE_CONTRACT.md)
libtorrent (in the torrent helper)The torrent session: peers, pieces and resume data. The helper applies each torrent's seeding policy and the seeding queue, and its supervisor keeps each torrent's latest resume data in memory (PO decisions on #62). The manager owns torrent records, and stores their policies, metadata and resume data, without trackers (#58)
The user's file systemFinished files, written atomically: download to a part file, verify, then rename without replacing anything (#43, #72)
Memory onlyCredentials, cookies and custom headers, as Secrets wiped when dropped. Never written to the database, logs, checkpoints or process arguments; saved in the system keychain only when the user asks, for that download (ADR-0015, #70, #71)

Contracts

The UI and the native side share engine-neutral types: download records, statuses, progress, TransferError, capabilities and error codes. They're defined once in Rust (crates/velocity-core/src/domain.rs) and validated in the UI by strict schemas (src/contract.ts). Contract fixtures generated from Rust keep the two in step, and COMMAND_API_VERSION stops a mismatched UI from starting. Details: CONTRACTS.md.

A direct download, start to finish

The details are designed and tested in the issues named in each step.

sequenceDiagram
    participant UI as React UI
    participant Shell as Desktop shell
    participant Manager as Manager (velocity-core)
    participant DB as SQLite
    participant Engine as Transfer engine (child process)
    participant Server
    UI->>Shell: add download (typed command)
    Shell->>Manager: validated request
    Manager->>DB: save the record and the queue
    Manager->>Engine: add(TransferSpec), when a slot is free
    Engine->>Server: probe: GET with Range bytes=0-(4 MiB−1)
    Server-->>Engine: 206 or 200, then the body
    Engine->>Server: further ranges as leases, with If-Range
    Engine-->>Manager: Checkpoint event, after each sync of the part file
    Manager->>DB: save the checkpoint
    Engine-->>Manager: Changed events: progress, statuses
    Manager-->>Shell: DownloadsSnapshot, at most every 250 ms
    Shell-->>UI: downloads-snapshot event
    Engine->>Engine: verify, flush, rename without replacing
    Engine-->>Manager: Completed, with what was verified
    Manager->>DB: save the record, drop the checkpoint
  1. Add. The UI sends the request. The core validates the URL (scheme allowlist, IDN handling, credentials moved out of the URL), the destination and the name, then stores the record and queues it (#31, #50, #58, #67).

  2. Start. When the queue allows, the manager passes a transfer spec through the DirectTransfer contract (#36). The queue is the manager's own (#50): at most maxConcurrentDownloads run at once (2 by default), waiting downloads start in queue order, and scheduled ones run only while the schedule's window is open in local time. A paused or finished download frees its slot. The engine is never given a download before its turn, so it has no queue of its own.

  3. Probe. The first request is a bounded range request whose bytes are kept. The response shows whether the server supports ranges, without a wasted full transfer (#41).

  4. Transfer. Segments are assigned as exclusive leases. Free workers split the largest remaining lease. Every byte is accepted only from the lease that owns it (#42, #47). Retries, back-off, stall detection and speed limits apply per transfer and globally (#48, #49).

  5. Checkpoint. Progress becomes durable only after the data is flushed to disk, so a crash resumes from trusted coverage (#43). When the server gave a strong validator, every range request carries it in If-Range, so a file that changed on the server is never mixed with the old one (#45). Without one, a resume is checked on the length alone and shows "not verified" (ADR-0010).

  6. Complete. The engine verifies coverage, length and, when a trusted hash exists, the whole-file hash. It flushes the file, then renames it without replacing an existing file. The status says "Verified" only when a hash was checked (#46).

Status reaches the UI as events: the manager sends a DownloadsSnapshot whenever it changed, at most every 250 ms (#58). The status vocabulary for direct downloads is Queued, Connecting, Downloading, Retrying, Stalled, Paused, Verifying, Completed, Failed and Removed; a cancelled download ends as Removed (#25, #36).

Processes and boundaries

ProcessStarted byTalks to
Desktop appThe userThe UI through typed commands and events, and the native host through local IPC (#65)
Transfer engineThe app, which starts it as a child process (ADR-0012)The app over inherited stdin and stdout pipes: no listening socket, no port, no shared secret (#39)
Torrent helperThe app, which starts it as a child process (ADR-0014)The app over the same kind of pipes and protocol. It also listens for BitTorrent peers and reaches trackers and peers on the network, as torrents need (#62)
Native hostThe browser, through native messagingThe app over local IPC that accepts only local clients

Everything that crosses a boundary is untrusted and validated on arrival: URLs, file names, server responses, browser messages and engine messages.

Identity and data folders: branding.json is the single source of Apollo Download Manager's identity. The data folder (%APPDATA%\dev.velocity.downloadmanager.v2 on Windows today), the single-instance key and the IPC pipe name all derive from the identifier, so nothing is shared with V1. Details and the side-by-side check: COEXISTENCE.md.

Open decisions

These decision issues shape the architecture. Until each is decided and recorded as an ADR, the parts that depend on it are marked pending above.

None is open now.

Decided since: libtorrent runs in a supervised helper process, on a child-process layer shared with the engine (ADR-0014); the engine runs as a supervised child process (ADR-0012); SFTP uses russh in its own crate behind a shared transport interface (ADR-0013, superseding ADR-0011); without a validator, a download resumes after a length check and shows "not verified" (ADR-0010).

The research these questions draw on is in docs/research/. The terms used here are defined in the glossary.