Backends
A backend is an existing open-source media server that owns one kind of media and one folder. SoundStorm installs each one in its own container, sets it up with nobody logging in, keeps the credentials it generated, and drives it through its API - so the person using SoundStorm never sees it.
The backends at a glance
| Backend | Kind | Image | Provisioned by | What SoundStorm uses |
|---|---|---|---|---|
| Navidrome | Music | deluan/navidrome | First-run admin form while no user exists | Subsonic search, streams, covers, albums, lyrics, ReplayGain, transcoding for beats |
| Jellyfin | Films, TV | jellyfin/jellyfin | Its startup wizard, a plain REST API | Search, playback negotiation, HLS transcoding, subtitles, episodes |
| Audiobookshelf | Audiobooks | advplyr/audiobookshelf | Root user, library, per-member users | Search, tracks, chapters, per-person listening position |
| Immich | Photos | immich-app/*:v3 | Admin sign-up, API key, external library | Thumbnails, previews, smart search, people, places, per-member accounts |
| Storyteller | Read-along timing | pinned by digest | A server action on /init, then tokens | Sentence-level alignment of ebook text to audiobook audio |
| AudioMuse-AI | Song analysis | pinned by digest | /api/setup before its first save | Moods, energy, tempo, "sounds like" |
| SoundStorm itself | Ebooks, documents | - | Nothing to provision; it scans the folders | EPUB and PDF metadata, covers, the reader's resources |
Jellyfin is provisioned once and registered as two sources sharing one token -
jellyfin (films) and jellyfin-tv (series and episodes) - because they are
separate Jellyfin libraries with different scrapers. That is why provision.buildSources
returns a slice.
Why these, and why delegate at all
Owning scanning, metadata, artwork and transcoding is years of work to arrive somewhere worse than what exists. So each media type goes to the best open server for it, and the test for whether SoundStorm owns a type itself is: is it self-describing and free of transcoding? EPUB is; video and HEIC photos are not. Navidrome rather than Jellyfin for music because its music model is first-class (multi-value artists, compilations, ReplayGain, a fast scanner). Jellyfin rather than Plex because Plex needs a plex.tv account, which no installer can create - fatal to the zero-keys promise. Emby went closed-source.
The adapter interface
Every backend is reached through an adapter in internal/source/<name> that
implements source.Source:
type Source interface {
ID() string // "navidrome", "jellyfin-tv" ...
Kind() media.Kind // exactly one kind per source
Search(ctx context.Context, q media.Query) ([]media.Item, error)
Health(ctx context.Context) error
}
Everything else is an optional interface a handler checks for with a type assertion. A source that cannot do something simply does not implement it, and the feature is absent for that shelf rather than broken. The main ones:
| Interface | What it answers | Implemented by |
|---|---|---|
Streamer, ArtProvider | An authenticated upstream Target for an item's bytes or artwork, which the server fetches and pipes through | All |
Negotiator, HLSProvider, SubtitleProvider | Whether a film needs transcoding, its HLS playlists and segments, WebVTT subtitles | Jellyfin |
TrackLister, ChapterLister, PositionTracker, AudioLayouter | An audiobook's files on one clock, its chapters, the listener's place, how its files map to chapters | Audiobookshelf |
MusicBrowser, MixSource, LyricsSource, SongFileLister, Listener | Albums and artists, random songs and genres, lyrics, real file paths, FLAC for beat analysis | Navidrome |
ShowBrowser | A show's seasons and episodes | Jellyfin (TV) |
PhotoBrowser | People, places, a person's photos | Immich |
BookOpener | A book's internal files for the reader | localbooks |
ItemGetter | One item by id, for favorites, playlists and Continue | All |
FileLister | Which files an item is, relative to its shelf, for deletion | Media backends |
RecentLister, InProgressLister | Newest first for Home, books in progress for Continue | Most |
Rescanner, Starter | "Look now" after an upload; work to do before answering at all | Most; localbooks |
A Target carries headers as well as a URL because backends disagree about where a
credential goes: Subsonic in the query string, Jellyfin 12 only in an Authorization header.
The proxy just replays both parts. It is never given to a browser.
Normalization at the edge
Only an adapter knows its backend's vocabulary. Everything past it speaks media.Item.
That rule absorbs a surprising amount of disagreement, each found against a live server:
- Browsing an empty query: Navidrome answers an empty
search3with everything; Jellyfin needssearchTermomitted, not blank; Audiobookshelf's search matches nothing for an emptyq, so browsing uses a different endpoint with a differently nested response. - Order: paging a merged list needs every source to return its first N in exactly
the merge's order, and no backend's own sort does - Navidrome's produced 1,760 repeats in 40 pages.
So browsing adapters fetch the whole shelf, order it with the one comparator
media.Less, and cut;media.ShelfCachekeeps it for 30 seconds. Immich is the deliberate exception, newest first, with aSortKeythat encodes it. - Paths: Navidrome reports them relative to the music folder, Audiobookshelf
gives a book folder, Immich and Jellyfin give paths as their containers see them, trimmed with
source.RelativeTo, which refuses anything outside the root. - Missing files: only Navidrome hides them by itself. Audiobookshelf keeps serving items whose files are gone, so its adapter filters them.
Rules every backend lives under
- A dead backend never takes search down: a deadline and an error slot per source,
always 200, and
degradedsays what is missing. A panicking adapter is recovered and reported the same way. - Reconnecting is not provisioning. On restart SoundStorm can beat a backend to listening; only a 401 or 403 means the credentials are wrong. A 5xx, 429 or dial error means wait. Treating those as "wrong password" once bricked a working backend.
- Access is per kind:
Registry.All,MatchingandByIDtake the request's context, which carries what the account may see, so a handler cannot reach a source without the restriction being applied. - No backend publishes a port. Only SoundStorm's port is published; the backends are reachable only on the compose network, and the shared HTTP client refuses absolute references, dot segments and redirects to another host, since its headers carry admin keys.