State and data

SoundStorm started out stateless and had to learn to remember. What it remembers is kept small and deliberately shaped: credentials, accounts and decisions in one file, each person's lists in their own, rebuildable caches beside them - and never a fact about the media, which belongs to the backends and the folders.

The line that matters

SoundStorm keeps nothing about the media itself. No index of songs, no copy of metadata, no list of what is in a folder. That is why nothing it keeps can go stale when somebody copies files in with a file manager.

Per-person things that are SoundStorm's own - favorites, playlists, film positions - are stored as references (a source and an id, with a snapshot of how the backend described the item), and are filtered through the registry each time they are shown, so a deleted song simply fails to play and a shelf an account has lost drops out.

Where everything lives

WhereWhatIf lost
state.jsonBackend credentials, accounts, sessions, reading positions, a few decisionsCannot be regenerated - the backends hold accounts whose passwords nobody else knows
collections/<user>.jsonFavorites, playlists, history counts, preferences, scrobble queueThat person's lists
collections/listens/Every play, with its moment, one file per person per yearThe year in music
collections/art/<user>/Covers people chose themselvesThose choices
beats/, lyrics/, discover/Caches: song analysis, LRCLIB answers, artist infoNothing - rebuilt
tls/Local authority, server certificate, Let's Encrypt certificateDevices that installed the authority must install it again
training/The developer's own taps for training the looks (one install only)Recordings
Library foldersThe media, plus hidden .uploads, .trash, .importsThe media - which nothing in SoundStorm ever deletes outright
Backend volumesEach backend's own database, thumbnails, transcode cacheRe-provisioning, or a lost password
The browserDownloads, the offline shell, small per-device settingsThat device's downloads

Everything in the first seven rows lives under the state directory, SOUNDSTORM_STATE_DIR, which is /var/lib/soundstorm in the container, backed by the soundstorm-state named volume.

state.json

Owned by internal/state, version 3 of its schema. It holds:

Why is "the starter library has unpacked" in here rather than a marker file? An emptied shelf and a never-used one look identical on disk, so without the flag somebody who deleted the samples got them back on every restart. A marker file in the library would be the one stray item at the top of a folder in Windows Explorer (which does not hide dot-files) - and deleting it, the natural thing to do, would bring the samples back.

The file is rewritten whole, to a temporary name and renamed, on every sign-in and session change, under a lock every request takes. That is why the large per-person data is not in it, and why every write path into it is bounded. Each save also leaves a .bak, which covers a bad write and nothing else.

state.Open migrates and saves every time it opens the file, so any process that opens it as another user takes ownership of it - and the server then crash-loops on "permission denied". The password-reset and restore commands stat the file first and hand ownership back; backup uses the read-only state.Inspect and state.CopyTo instead.

Each person's collections

internal/collections keeps one small file per person: favorites (up to 5,000), playlists (200, of up to 5,000 songs each, 25,000 in all), listening history (count, first and last play, 5,000 songs), preferences such as the pill order and Now Playing look, and the ListenBrainz queue. One change writes one small file. Removing a person removes their file, art and listens.

Favorites and playlists are SoundStorm's rather than Navidrome's, although Navidrome has both: the house shares one Navidrome account, so lists kept there would be everybody's at once. The same reasoning keeps film positions in state.json rather than in Jellyfin.

Listens are append-only JSON lines, one file per person per year (listens/<user>-2026.jsonl), capped at 32 MB a year. Recording costs one small write; a damaged last line from a crash is skipped.

Art is pictures people chose for a song, an album or a playlist, cropped and shrunk on the device, named by content so one picture for an album is stored once; 5,000 covers, 1,000 pictures and 300 MB per person, served only to its owner.

Caches

In memory only: shelf listings (30 seconds), slim song layouts (an hour), shrunk covers, and a Plex token during an import (30 minutes, never written down).

The library folders

The media is the person's, in plain folders: music/, movies/, tv/, audiobooks/, ebooks/, documents/ and pictures/, under SOUNDSTORM_LIBRARY_PATH. Three hidden top-level folders sit beside them, hidden for the same reason: no backend has a top-level dot-directory mounted, so whatever is in them is invisible to every scanner.

A README.txt in each shelf is load-bearing: Jellyfin refuses to remove items when a library folder comes back empty (it cannot tell "everything deleted" from "the drive did not mount"), so an empty folder would keep deleted films searchable for ever.

The backends' own volumes

Each backend keeps its database and caches in named volumes - navidrome-data, jellyfin-config, abs-config, immich-db and so on. These are the backends' business; SoundStorm never reads them. Immich's Postgres in particular must sit on a named volume, because it must not live on a network share or an NTFS drive. Caches and downloaded models (jellyfin-cache, immich-models, storyteller-models) are left out of a move to another computer.

The browser and the apps

Signing out clears downloads; the device also remembers whose they were and clears them for anybody else who signs in.

Backup and restore

soundstorm backup writes the state file plus a collections field with everyone's lists. A field rather than a wrapper, so a backup still is a state file: an older SoundStorm restores the accounts and ignores the lists, and with no lists the backup is the state file byte for byte. Pictures and listens are not included; moving to another computer copies the whole state volume instead.

(umask 077; docker compose run --rm -T soundstorm backup - > soundstorm-backup.json)
docker compose run --rm -T soundstorm restore - < soundstorm-backup.json
docker compose up -d

With - the backup goes to standard output, so the host shell creates the file as the user, under umask 077 - a file written through a bind mount by the container's own user would be unreadable to whoever asked for it on Linux (Windows, where Docker Desktop presents bind-mounted files as the user's, uses a bind mount and a locked-down ACL instead). backup - refuses a terminal so credentials never scroll past on screen. Restore checks the lists before touching anything, so a damaged backup changes nothing. The uninstaller takes a backup before down -v, the one moment the credentials would otherwise stop existing anywhere.