Architecture
How the pieces fit together: what each crate owns, how a share moves from a tagged *arr item to a friend’s Sonarr, where the trust boundaries sit, and where state lives. The design brief explains why sharerr is shaped this way; the lighthouse doc and the API reference go deeper on two parts this page only summarises.
Table of contents
Crate map
Twelve crates under crates/, one workspace, two binaries (sharerr and sharerr-lighthouse). This table is the one place the crate list is kept; everything else links here.
| Crate | Owns |
|---|---|
sharerr | The binary: CLI, web UI, Torznab/Jackett, tracker, reconciliation, directory libraries, gossip, lighthouse client, notifications |
sharerr-core | Domain types, layered config, path mapping. No I/O, so every other crate can depend on it without pulling in a client, a database, or a web framework |
sharerr-arr | Sonarr/Radarr/Lidarr/Readarr/Whisparr clients and tagged-content discovery |
sharerr-client | The narrow TorrentClient trait a backend implements. sharerr talks to whichever backend is configured through this one interface, so a backend can be swapped without touching the reconciliation loop |
sharerr-qbit | qBittorrent WebUI client |
sharerr-transmission | Transmission RPC client |
sharerr-rtorrent | rTorrent XML-RPC client |
sharerr-store | Encrypted vault + SQLite store |
sharerr-torrent | Torrent construction and tracker resolution |
sharerr-probe | Reads what a media file is, where no *arr can say |
sharerr-lighthouse | The lighthouse rendezvous service, its own binary and image. Deliberately independent of the rest of the workspace: a separate service with a separate threat model, not a module of the main binary |
sharerr-testkit | Synthetic fixtures. Never in a release build |
How a share moves
flowchart LR
arr["*arr app<br/>(Sonarr / Radarr / ...)"] -->|"tagged sharerr"| discover["sharerr-arr<br/>discovery walk"]
discover --> probe["sharerr-probe<br/>(only if no *arr metadata)"]
discover --> build["sharerr-torrent<br/>build in place"]
probe --> build
build -->|"add, do not move"| client["torrent client<br/>via sharerr-client"]
build --> feed["Torznab feed"]
feed -->|"Prowlarr indexes it"| friend["friend's Prowlarr<br/>+ Sonarr/Radarr"]
client -->|"announce"| tracker["sharerr's own tracker"]
friend -->|"announce, with a peer key"| tracker
gossip["gossip"] <-->|"Ed25519-signed records"| friend
gossip -.->|"when direct contact is lost"| lighthouse["lighthouse<br/>rendezvous"]
- Discovery:
sharerr-arrwalks the configured *arr instances for items taggedsharerr, carrying whatever TVDB/TMDb/IMDb metadata the app already has. - Metadata fallback: for a plain directory with no *arr app behind it,
sharerr-probereads the media file itself. Seedocs/SUPPORT.mdfor which formats. - Torrent construction:
sharerr-torrentbuilds a.torrentdescribing the file where it already sits. Never copied, renamed, or re-linked: the constraint the design brief is built around. - Seeding: the torrent is added to the configured client through the
sharerr-clientcontract, withskip_checkingset where the client supports it so it seeds immediately rather than re-verifying data sharerr never wrote. - Publishing: the release is served over sharerr’s own Torznab feed. A friend’s Prowlarr indexes it; their Sonarr/Radarr match the release by id, not by parsing a filename.
- Tracking: sharerr runs its own BitTorrent tracker (see the README). Announces authenticate with a peer’s key, and the tracker fails closed on any vault or database failure.
- Finding each other: gossip exchanges signed endpoint records directly between friends; when both addresses rotated at once, the lighthouse is the fallback. See
docs/LIGHTHOUSE.mdfor why it can do that without learning who is asking.
Trust boundaries
- Operator ↔ vault: the only secret the operator holds outside sharerr is
SHARERR_MASTER_KEY. Everything downstream of it (*arr keys, client credentials, the tracker token, the gossip signing key) is encrypted at rest bysharerr-store’s vault and never written tosharerr.toml. The one documented, self-deleting exception is the[[peers]]restore block; seedocs/SETTINGS.md. - This instance ↔ a friend: a friend authenticates with a peer key this instance issued them, scoped to what that key can see (
PeerScope). Gossip records are Ed25519-signed by the peer they describe, so a relay in between can drop a record but never forge or replay an older one. - This instance ↔ the lighthouse: treated as an untrusted, semi-public relay. It holds no proof it could not itself forge for an unauthenticated query (see the lighthouse doc), and anything it returns ranks below a direct sighting of the same peer.
- This instance ↔ the services it drives: *arr apps, the torrent client, and gluetun are assumed to be under the same operator’s control, on the trusted network sharerr is meant to run on. See
docs/SECURITY.mdfor the threat model that boundary sits inside.
Where state lives
sharerr-store: one SQLite database underdata_dir(shared_items,sync_runs,users,peers,peer_endpoints,swarm_samples) plus the encrypted vault file and the generated.torrentfiles.sharerr.toml: non-secret configuration only. Rewritten in place by the web UI viatoml_edit, comments and all.- In memory only: session tokens (a restart revokes every session).
- Nothing outside the configured services: no telemetry, no external calls beyond what an operator configured. See the design brief for how that property is tested.
Refactors weighed and declined
Candidates from a whole-codebase simplify pass, checked and rejected. Kept so the same shape is not re-proposed by a later pass over the same code.
- The three
poll_loops (system_stats.rs,gluetun.rs,swarm_history.rs) share onlyloop { work; sleep }over different bodies and intervals. Unifying them is over-abstraction. tracker.rs’s#[allow(dead_code)]onAnnounceParams/ScrapeParams: documentation-only utoipa shapes with a stated reason.doctor.rsvschecks.rs: the parallelcheck_*names are not duplication;doctor.rsdelegates intochecks::and wraps for reporting.sharerr-transmissionas one file: at ~550 production lines it is under the threshold where a module split pays for itself.sharerr-probe’s two metadata loops (Matroska, ISO-BMFF): track types, codec accessors, and theund-language case differ enough that sharing costs more than it saves.sharerr-probevsMediaMeta::scene_*: the probe deliberately does not duplicate core’s scene-token mapping; the split is documented insharerr-probeitself.sharerr-arr’sapi_prefixrestatesMediaSource::api_version; both sides carry comments arguing for the split.- A shared trait across the torrent-client backends already exists (
sharerr-client’sTorrentClient) at the right altitude. - Collapsing
Config::torrent_client_for’s three match arms: only 2 of its 10 fields are backend-agnostic. Every extraction tried cost more in indirection than the lines it removed.