The server
One Go program, cmd/soundstorm, sits between every app and every
backend. It signs people in, searches every shelf at once, carries every byte of every song,
film and photo, files what is dropped onto the window, and remembers the few things that
belong to a person rather than to the media.
One program, no dependencies
The server is a single static binary built from the standard library alone: there is no
go.sum, because there are no third-party Go modules - not for password hashing
(crypto/pbkdf2), not for the ACME client that gets real certificates, not for the
FLAC decoder the beat analysis uses. The web app is embedded in the same binary with
go:embed, so the image is the program and nothing else. The only third-party code
in the whole project is foliate-js, the EPUB renderer, vendored into the web assets.
It runs in a container beside the backends it drives, as an unprivileged user (uid 10001), with no access to the Docker socket. That last point is a choice, not an omission: a server that cannot drive Docker cannot be turned into a way onto the host, which is also why it cannot update itself - updating is running the installer again.
Besides serving, the same binary carries a few subcommands that run beside a stopped or
running server: reset-password, backup, restore and
train-looks. Any other argument is an error rather than a second server, because a
typo once started two writers on one state file.
What happens at start
The order in main is deliberate: everything a person needs to see the app is done
before the port opens, and everything that waits on another container happens in the
background afterwards.
- The library folders.
library.Openmakes suremusic/,movies/,tv/,audiobooks/,ebooks/,documents/andpictures/exist, so a fresh install already has somewhere to put things. - The state file.
state.Openreads (and migrates) the one file holding backend credentials, accounts, sessions and a handful of decisions. Never anything about the media itself. - The starter library - one public-domain or CC0 item per stocked shelf, about 17MB embedded in the binary - is unpacked into empty folders once, ever. A flag in the state file records that it happened, so somebody who deletes the samples does not get them back at the next restart.
- Housekeeping. Half-finished uploads in the hidden staging folder are cleared, the per-person collections folder is opened, and a daily sweep empties the bin of anything deleted more than thirty days ago.
- TLS is loaded in whichever mode
SOUNDSTORM_TLSnames: off, self-signed (a local authority), file, or auto (a real certificate). An unknown value is fatal - quietly serving plain HTTP to somebody who asked for encryption is the worst way to be wrong. - Provisioning starts in the background. Each backend named in the environment is set up, or reconnected with stored credentials, in its own goroutine. A backend can take a minute to boot, and SoundStorm should be showing setup progress meanwhile, not refusing to start. So sources join the registry as they become ready.
- The real certificate is fetched in the background too, in auto mode.
- A setup code is printed to the log while no account exists - the first sign-up needs it.
- Background loops start: automatic read-along syncing, sending plays to ListenBrainz, hearing every song's beats ahead of time, and sorting imported photo downloads.
- The listener opens - on one port that speaks both plain HTTP and TLS in auto mode, told apart by the first byte of each connection.
recover(). Go's net/http protects only the goroutine serving a request;
a panic anywhere else ends the whole process, for everybody. A parser that trips over one
strange file on a timed scan would otherwise crash the server on that file, and again on
every restart, because the file is still on disk.Package map
Packages live under internal/. The rule that keeps them apart is
normalization at the edge: only an adapter knows its backend's vocabulary, and
everything past it speaks media.Item.
| Package | What it owns |
|---|---|
httpapi | Every route, the middleware chain, rate limits, and the handlers that tie the rest together. |
auth | Password hashing, sessions and their cookies, device tokens, the sign-in throttle, per-kind access. |
state | The one JSON file: backend credentials, accounts, hashed sessions, reading positions, a few owner decisions. |
source, source/* | The Source interface and its optional extras, the registry that enforces access, and one adapter each for Navidrome, Jellyfin, Audiobookshelf, Immich, Storyteller, AudioMuse-AI, OPDS and the local book folders. |
federate | Asking every source at once with a deadline each, and merging the answers into one ordered page. |
media | media.Item, the one comparator (media.Less) and the shelf cache. |
stream | Proxying media and art: ranges, picture-free song headers, pacing, cover shrinking, active-content guards. |
provision | Setting up each backend with nobody logging in, reconnecting after restarts, per-member backend accounts. |
library | The folders, where a dropped file goes, staging and atomic placement, the bin, free space. |
collections | Per-person favorites, playlists, listening history, the listen log, custom covers, preferences. |
servetls, acme, names, portmap | Certificates, the in-house ACME client, the name service and its client, router port mapping. |
beats, flac | Hearing each song once on the server for the visualizers, through a FLAC decoder of its own. |
tags, epub, pdf, photoimport | Small readers that answer one question each - where to file a file, what a book is called, when a photo was taken. |
lyrics, discover, scrobble, plex | The opt-in outside services: LRCLIB, MusicBrainz and friends, ListenBrainz, and Plex playlist import. |
starter, webui | The embedded starter library and web app. |