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 (incl. per-torrent rate limits, seed policy, sequential flag), user-added trackers, categories, tags, settings, schedule rules, feeds, filters, fast-resume snapshots, users + sessions + shares (mosaicd).
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, schedule engine, and watch-folder watcher are not separate top-level packages — they live inside backend/api/ and are constructed with api.NewRSSPoller / api.NewScheduleEngine / api.NewWatchFolder, 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; the 5-second directory poll for new .torrent files lives in backend/api/watchfolder.go. The seed-limit ticker (30 s) and the per-torrent rate-limiter duty-cycle goroutine (500 ms) live alongside the engine.

The engine additionally runs a periodic fast-resume checkpoint (5-minute tick) that re-saves the piece-completion bitmap for every active torrent. The checkpoint lets restart-without-clean-shutdown still pick up the fast-resume path — without it, a crashed process would dump its in-memory bitmap and force the next startup to re-hash every file.

Data flow: a tick

Once a second, the desktop shell and the HTTPS hub re-broadcast three frames:

  1. torrents:tickapi.Service.ListTorrents() snapshots the engine, joins persistence rows for category_id + added_at, fans out per-torrent tags from the SQLite torrent_tags join, and emits a []TorrentDTO.
  2. stats:tickapi.Service.GlobalStats() walks the same engine snapshot and emits a single GlobalStats.
  3. 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), plus per-torrent state: queue position, force-start flag, paused flag, sequential-download flag, per-torrent down/up rate limits (bytes/sec), seed-policy JSON, seeding-started-at timestamp, category id, and user-added trackers (via a separate torrent_trackers join). RestoreOnStartup re-adds every persisted torrent on launch and re-applies the per-torrent overrides: the engine resumes from on-disk pieces if they’re still there (using the fast-resume bitmap if available; full re-hash otherwise), 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 via runtime.EventsOn.
  • Browser mode — running against the HTTPS server. REST goes over fetch, the WebSocket connects to /api/ws, frames are dispatched by type to 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.