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
| Component | Path | Responsibility | Today | Built in |
|---|---|---|---|---|
| UI | src/ | React and TypeScript. It talks to Rust only through typed commands and events, and validates every native response before use | The 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 shell | src-tauri/ | Tauri 2 app: thin typed commands that delegate to the core, the window, the tray, notifications, the single instance and the browser bridge | app_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 |
| Core | crates/velocity-core/ | Everything that doesn't need a window: domain models, the download manager and queue, input validation, naming, categories, settings, history and SQLite persistence | app_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 contract | crates/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.md | The trait, its wire commands, a test-only fake engine, and the manager (manager/) | #36 |
| Transfer engine | crates/velocity-transfer/ (ADR-0002) | The range probe, leased segmentation, durable checkpoints, changed-object detection, integrity, retries, connection budgets and speed limits | The 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 layer | crates/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 decides | Links 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 layer | crates/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 refused | No 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 process | velocity-transfer serve, a child process (ADR-0012) | The engine runs as a child process the app starts and supervises, speaking a versioned protocol over stdio | velocity-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 layer | crates/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 checks | Both children run on it; each supplies a contract and a book for its supervisor (ENGINE_PROTOCOL.md) | #39, #62 |
TorrentEngine contract | crates/velocity-core/ (torrent_engine.rs) | The boundary for torrents: commands, events and snapshots in Apollo DM's types. Torrents never pass through DirectTransfer | Version 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 helper | crates/velocity-torrent/ (ADR-0014) | What reaches libtorrent and what its state means; the helper velocity-torrent serve, its client and its supervisor | Downloads 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 bridge | crates/velocity-lt/ (ADR-0002) | The only crate that calls libtorrent: a small C++ shim over one session. It never deletes files | libtorrent 2.0.15, built with vcpkg (BUILDING_NATIVE.md) | #62 |
| Native host | crates/velocity-native-host | A short-lived process started by the browser. It validates messages and forwards them to the app over local IPC | Since #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 extension | browser-extension/ | Offers browser downloads to Apollo DM and falls back to the browser on any failure | Since #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'sAGENTS.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.
| Owner | State |
|---|---|
| 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 engine | Active 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 system | Finished files, written atomically: download to a part file, verify, then rename without replacing anything (#43, #72) |
| Memory only | Credentials, 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
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).
Start. When the queue allows, the manager passes a transfer spec through the
DirectTransfercontract (#36). The queue is the manager's own (#50): at mostmaxConcurrentDownloadsrun 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.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).
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).
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).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
| Process | Started by | Talks to |
|---|---|---|
| Desktop app | The user | The UI through typed commands and events, and the native host through local IPC (#65) |
| Transfer engine | The 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 helper | The 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 host | The browser, through native messaging | The 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.