Architecture
Process layout, package boundaries, and the data flow from peer wire to UI tick.
Mosaic is a single Go binary that embeds a Wails-managed webview (or, when the remote interface is enabled, an HTTPS server) and serves the same SolidJS SPA either way. The engine is anacrolix’s BitTorrent library; persistence is SQLite.
Process layout
+--------------------------------------------------------------+
| Mosaic process |
| |
| +--------------------+ +----------------------+ |
| | Wails desktop | | remote (chi router) | |
| | shell | | /api/... | |
| | - native window | | /api/ws (WebSocket) | |
| | - tray + notify | | / (static SPA) | |
| | - file assoc. | +----------+-----------+ |
| +---------+----------+ | |
| | | |
| v v |
| +---------------------+ +--------------+--------------+ |
| | api.Service |<->| remote.Hub (WS fan-out) | |
| | (DTOs + commands) | +-----------------------------+ |
| +---------+-----------+ |
| | |
| v |
| +---------------------+ +-----------------------------+ |
| | engine | | persistence (SQLite) | |
| | (anacrolix wrap) | | - torrents | |
| | - DHT/PEX/uTP | | - categories / tags | |
| | - piece priorities | | - settings (KV) | |
| | - per-peer stats | | - schedule rules | |
| | - blocklist filter | | - feeds + filters | |
| +---------+-----------+ +-----------------------------+ |
| | |
| v |
| Network (TCP + uTP, DHT UDP, tracker HTTP) |
+--------------------------------------------------------------+
The desktop shell and the HTTPS server are two front-ends to the same api.Service; both produce identical DTOs.
Package layout
| Package | Role |
|---|---|
backend/engine |
Wraps anacrolix. Adds Snapshot, DetailedSnapshot, file priorities, IP blocklist filter, queue + force-start state, FilesMissing detection. |
backend/api |
The Service type — every command + DTO the UI/API speaks. No HTTP, no Wails. |
backend/remote |
HTTP layer: Mount (chi router), AuthGate, OriginGuard, SessionStore, Hub (WS), self-signed cert generator, login rate limiter. |
backend/remote/cred |
Argon2id HashPassword/VerifyPassword and RandomToken. Leaf to avoid an import cycle. |
backend/persistence |
SQLite schema + queries. Torrents, categories, tags, settings, schedule rules, feeds, filters. |
backend/updater |
Source + Updater over creativeprojects/go-selfupdate. SHA-256 manifest verification. |
backend/events |
Generic typed pub-sub bus used by the WS hub. |
frontend/src/lib/transport |
Browser-mode REST/WS or Wails-mode IPC selector. |
frontend/src/lib/bindings |
Type-safe api.* and on*Tick wrappers around transport. |
The RSS poller and schedule engine are not separate top-level packages — they live inside backend/api/ and are constructed with api.NewRSSPoller / api.NewScheduleEngine, sharing the same Service reference the rest of the API surface uses. ETag-aware feed fetch + regex filter dispatch live in backend/api/rss_poller.go; time-of-day rule evaluation + bandwidth-limit reapplication live in backend/api/schedule_engine.go.
Data flow: a tick
Once a second, the desktop shell and the HTTPS hub re-broadcast three frames:
torrents:tick—api.Service.ListTorrents()snapshots the engine, joins persistence rows forcategory_id+added_at, fans out per-torrent tags from the SQLitetorrent_tagsjoin, and emits a[]TorrentDTO.stats:tick—api.Service.GlobalStats()walks the same engine snapshot and emits a singleGlobalStats.inspector:tick— only fired if the UI has set a focus (SetInspectorFocus(id, tabs)). The backend reads the visible tabs and asks anacrolix for just the scoped detail (files / peers / trackers, on demand).
Update-availability frames (update:available) and one-shot launch:notice frames are pushed asynchronously when the relevant subsystems fire.
The WS bus is bounded: per-client send channels are 64-deep, and the Hub.broadcast loop drops frames for slow clients rather than blocking the producer. There is no replay — clients catch up on the next tick.
Persistence
A single SQLite file at <DataDir>/mosaic.db (the anacrolix engine/ cache lives in the same DataDir). Schema versions migrate forward on startup; downgrades aren’t supported.
The settings table is a generic (key TEXT PRIMARY KEY, value TEXT) KV. Bools are stored as "true"/"false" strings; ints as decimal strings. api.Service has helpers (boolSetting, intSetting, boolSettingDefault) that paper over presence semantics so a fresh DB returns the right defaults.
Torrent records carry both the magnet (if added by magnet) and the metainfo blob (if added by file). RestoreOnStartup re-adds every persisted torrent in either form on launch — the engine resumes from on-disk pieces if they’re still there, or flags FilesMissing and pauses the torrent if the user deleted the data between sessions.
Frontend transport
frontend/src/lib/transport.ts picks one of two implementations:
- Wails mode — running inside the desktop shell. Calls go through the bound
Mosaic.App.*methods; events are received viaruntime.EventsOn. - Browser mode — running against the HTTPS server. REST goes over
fetch, the WebSocket connects to/api/ws, frames are dispatched bytypeto the same handlers Wails events would have hit.
The result: bindings.ts (api.listTorrents(), onTorrentsTick(...)) is the only surface the rest of the SPA uses. Switching transports is invisible to feature code.
Update path
The updater goroutine starts shortly after launch (gated by Updater.Enabled), runs an initial check, and re-checks every 24 hours. On finding a newer release, it fires the configured OnAvailable callback (which fan-outs an update:available WS frame and a desktop notification, if enabled), but does not auto-install — the install step is user-initiated from the toast or the Settings → Updater pane.
The install path runs selfupdate.UpdateTo with the lib’s ChecksumValidator, which fetches SHA256SUMS from the release and verifies the downloaded asset before swapping the running binary. See Auto-Update for the full per-platform mapping.