How it is built

Architecture overview

The three parts of Nuvo, how they talk, and where each kind of data lives.

Nuvo has three parts.

Part Technology Job
Window Electron renderer, React, TypeScript Screens, the playback engine, the equalizer, lyrics display
Main process Electron main Starts the daemon, owns the tray, the Notch, file dialogs, the security policy
Daemon nuvo-core Rust, axum, SQLCipher Library, playlists, history, caches, lyrics, ranking, imports, local files

Window Electron renderer, React

Screens
Engine: decks, equalizer, queue
YouTube access
Electron maintray, Notch, file dialogs, policy
nuvo-coreRust, axum
library.dbSQLCipher
audio cacheXChaCha20-Poly1305
cover cache
keyring
Outside servicesYouTube Music, SoundCloud, LRCLIB, link APIs
yt-dlp, ffmpegoptional

The renderer (React) holds three things: the screens, the engine (decks, equalizer, queue) and the YouTube access (youtubei.js). The screens feed the engine. The screens talk to Electron main over IPC in both directions, and reach the daemon through the nuvo-core:// protocol that main handles. Electron main talks to nuvo-core (axum) in both directions, over HTTP on a unix socket with a bearer token. The daemon writes to the library database library.db (SQLCipher), the audio cache (XChaCha20-Poly1305) and the cover cache, and is linked to the keyring. It reaches the outside services (YouTube Music, SoundCloud, LRCLIB and the link APIs) and spawns the optional helpers yt-dlp and ffmpeg. The YouTube access in the renderer reaches the same services directly.

Startup

  1. Main generates a random token, then spawns nuvo-core with it.
  2. The daemon binds a unix socket in a private directory and answers /health with starting.
  3. It reads the library key from the keyring and opens the database. When it finishes, /health says ok.
  4. If the key is missing for an existing database, the daemon starts locked and waits for the recovery phrase.

Main polls health every 50 ms at first. If the keyring shows an unlock prompt, the daemon keeps answering starting and main waits up to two minutes. A daemon that never answers is treated as dead after five seconds. After a crash main restarts it after 0.5, 2 and 5 seconds, and gives up after a fourth crash within a minute. Playback keeps going while the daemon restarts.

When Electron dies, the daemon notices that its standard input closed and exits.

How the window reaches the daemon

The window never sees the token. It calls nuvo-core:// URLs. Main checks them and forwards them to the unix socket with the token. See daemon API.

Where the work happens

  • Playback runs in the renderer. Two audio elements (decks) feed one audio graph: normalization gain, preamp, ten filters, a reverb send, and the output.
  • The daemon fetches streams only from allowed playback hosts and stores them in the encrypted cache. See audio cache.
  • Search, related tracks and stream URLs for YouTube come from the renderer’s YouTube client. SoundCloud’s public catalog is read by the daemon.
  • Ranking of recommendations runs in the daemon over candidates the renderer fetched.
  • Lyrics come from LRCLIB through the daemon.
  • yt-dlp and ffmpeg are optional helpers the daemon spawns for link inspection, recognition and saving. Playback does not need them.

Crates in the daemon

Crate Purpose
nuvo-server The HTTP API, state, scheduling
nuvo-store SQLCipher database, migrations, queries
nuvo-crypto Key derivation, chunk encryption, recovery phrase, keyring
nuvo-cache Encrypted audio files
nuvo-covers Cover art at display size
nuvo-lyrics LRCLIB client
nuvo-recs Rewards, ranking, mixes, suppressions
nuvo-match Decides whether a catalog track is the same recording
nuvo-import Parsers and exporters for playlist files
nuvo-local Folder walking, tags, saved copies
nuvo-fetch yt-dlp, ffmpeg, proxy handling, link safety
nuvo-soundcloud SoundCloud’s public catalog and streams
nuvo-recognize Shazam signatures and the listening plan
nuvo-lastfm Last.fm client
nuvo-net, nuvo-contracts Network helpers, shared types