Self-hosted music server — one Rust binary, SQLite, and an OpenSubsonic API your clients already speak
WaveFlow Server streams the music you already own to the clients you already use. It scans your folders, owns the catalogue in a single SQLite file, transcodes on demand with FFmpeg, and answers on three surfaces at once: the OpenSubsonic API that dozens of existing players speak, a native API for WaveFlow Desktop, and an embedded web player compiled into the binary.
No PostgreSQL. No Redis. No identity provider. No container orchestration. One binary, one database file, one key file.
Status —
2.0.0-beta.1. All six milestones pass their release gates, and the OpenSubsonic façade has been replayed against four real clients — Symfonium 15.0.1 and DSub 5.5.3 on an Android 17 emulator, Feishin 1.15.1 on Windows desktop, and Juliet on a physical iPhone — with every result read back from server state rather than from what the client displayed. See the compatibility matrix for what each one actually exercises, and what it does not.
Requirements: ffmpeg and ffprobe on PATH. Nothing else — no database server, no cache, no broker.
docker run -d --name waveflow \
-p 4533:4533 \
-v waveflow-data:/data \
-v /path/to/music:/music:ro \
ghcr.io/instazdll/waveflow-server:2.0.0-beta.1Or with Compose — set WAVEFLOW_MUSIC_PATH to your music directory, which is mounted read-only:
WAVEFLOW_MUSIC_PATH=/path/to/music docker compose up -dThen create the admin account and register a library:
docker exec -e WAVEFLOW_ACCOUNT_PASSWORD='at-least-twelve-characters' \
waveflow waveflow-server account create-admin --username admin
docker exec waveflow waveflow-server library add --owner admin --name Music --path /musicOpen http://your-host:4533 and sign in. The scan starts on its own.
Requires Rust 1.94+ as well.
bun --cwd=webapp install
bun run build # webapp first, then cargo — the order matters
export WAVEFLOW_ACCOUNT_PASSWORD='at-least-twelve-characters'
cargo run -- account create-admin --username admin
cargo run -- library add --owner admin --name Music --path /path/to/music
cargo runcargo build works without a client build too: a placeholder page is embedded when webapp/dist is absent.
A Subsonic client uses a dedicated password, separate from the web account's:
export WAVEFLOW_SUBSONIC_PASSWORD='a-different-app-password'
waveflow-server credential set --actor admin --username adminPoint any Subsonic client at http://your-host:4533 with that username and password. Browser-hosted clients need their origin listed, and a reverse proxy needs WAVEFLOW_PUBLIC_URL — both are in the operations guide.
| One process | src/main.rs loads config and serves; everything else lives behind app() in src/lib.rs, so the test suite drives the very router main does |
| One database | SQLite in WAL with foreign keys and a single process-wide writer. Migrations are dated, embedded at compile time and checksummed — editing an applied one makes the server refuse to start |
| One catalogue, three surfaces | /api/v2, the Subsonic façade and the web client all call the same domain services. A mutation reachable from two surfaces calls one method, which is what stops them drifting |
| Tenancy in the query | Enforced through library membership inside the SQL, never in a handler. A resource that is missing and one that belongs to somebody else answer identically — a 404 never confirms existence to someone not entitled to it |
| Read-only files | The scanner and every tag operation leave your audio files untouched |
Your identifiers stop moving. Album and artist IDs are derived from the tags that name them (UUID v8 over a configurable spec, the same grammar Navidrome uses). Rebuild your database from scratch and the same files answer with the same IDs — so cached artwork, starred albums and deep links survive a reinstall. Track IDs are drawn at random on purpose: six tables cascade off them, and a scan matches a file by path and then by content hash, which is a better identity than any tag.
The Subsonic surface is the reference's, not a variant. Where WaveFlow and Navidrome disagreed on the artist model, we withdrew — thirteen credit roles, contributors[], displayComposer, roles[], an album that hangs off every artist it is credited to, and separator rules that split Rue Delacour / Ivy Trench in two while leaving AC/DC alone.
| Area | Highlights | Deep dive |
|---|---|---|
| Catalogue | Authoritative scanner with content hashing and relocation detection, FTS5 full-text search that folds case and diacritics, deduplicated artwork, embedded and sidecar lyrics, extended tags (ISRC, BPM, moods, ReplayGain, explicit status) | RFC-002 |
| Credits | Thirteen roles from the reference model — artist, album artist, composer, lyricist, conductor, arranger, producer, director, engineer, mixer, remixer, DJ mixer, performer with its instrument — one person can hold several on one track | Subsonic guide |
| Streaming | Original byte-range playback, on-demand FFmpeg transcode to MP3 or Opus with a disk cache, per-user and global concurrency limits, temporal seek into a live transcode | Subsonic guide |
| OpenSubsonic | The full browse, search, playlist, favourite, rating, scrobble, bookmark, play-queue and share surface, in XML or JSON, over GET or form POST, with the extensions it advertises | compatibility matrix |
| Native API | /api/v2 with rotating sessions, Authorization Code + PKCE for desktop clients, user-data sync over REST and WebSocket, SSE scan progress, an OpenAPI document and an interactive reference |
API v2 guide · RFC-003 |
| Uploads & canvas | A library can accept files, decided server-side and received in chunks; a track can carry a looping visual with its own store and tickets | RFC-008 · RFC-009 |
| Scrobbling | ListenBrainz, Last.fm and Maloja, each a named instance, behind a durable queue that survives a restart, retries once and honours Retry-After |
RFC-010 |
| Web player | React 19 client compiled into the binary — complete player and administration surface, authenticated artwork, Media Session, preloading, keyboard controls, 14 localized themes, English and French, responsive | — |
| Security | Argon2id passwords, tokens stored only as SHA-256 hashes, the dedicated Subsonic password encrypted with ChaCha20-Poly1305 under a local instance key, stream tickets so <audio src> needs no header, origin validation and CSRF protection for browsers |
operations |
| Operations | Single-file SQLite in WAL with one process-wide writer, immutable checksummed migrations, coherent backup and restore of the database/key pair, /health and /ready, JSON logging that never records a query string or a token |
operations |
| Run it | Operations — backup, losing or rotating the instance key, reverse proxies, probes and logging |
| Integrate | Native API v2 · Subsonic / OpenSubsonic |
| What clients do | Compatibility matrix — the four replayed above, plus Substreamer as a historical row: that build no longer installs on a current device, so it is not counted toward a tag · gap analysis |
| What changed | CHANGELOG |
| The design | RFC-002, the accepted design · RFC-003, sync · RFC-004, local/server reconciliation · RFC-007, library events · RFC-008, uploads · RFC-009, canvas · RFC-010, scrobbling |
| On a running server | /reference for the interactive API · /openapi.json for the contract |
| Contribute | CONTRIBUTING.md · CLAUDE.md for the conventions in depth |
An RFC's
Statutfield never flips — every one saysProposedwhether it shipped a year ago or not at all. What each carries instead is anImplémentée parline naming the pull requests. When it matters, read the code: the routes registered insrc/lib.rsand the dated files undermigrations-v2/are the only account of what exists.
| Layer | Technologies |
|---|---|
| HTTP | axum 0.8 (with WebSocket), tower-http, utoipa 5 for the OpenAPI document |
| Storage | SQLite through sqlx 0.9 — WAL, foreign keys, busy_timeout, FTS5, one process-wide write coordinator |
| Media | FFmpeg and ffprobe as external processes, lofty 0.25 for tags and embedded art, BLAKE3 for content hashing |
| Identity | UUID v4 for tracks, UUID v8 derived from tags for albums and artists, MD5 as the deduplication digest of the identity spec |
| Security | Argon2id, SHA-256 token digests, ChaCha20-Poly1305 for reversible credentials, AEAD-sealed stream tickets |
| Web client | React 19, TypeScript, Vite 8, compiled into the binary by rust-embed 8 |
| Runtime | tokio, tracing with optional JSON output |
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features # hermetic — temporary SQLite databases, no service container
bun --cwd=webapp run test
bun --cwd=webapp x playwright install chromium
bun --cwd=webapp run test:e2eFFmpeg and ffprobe must be on PATH: the suite boots a real media service. Commits need DCO sign-off (git commit -s) and Conventional Commit messages.
The v1 PostgreSQL/JWKS implementation was removed once the native API landed. It remains in git history; any reference to /api/v1 in an older document is stale.
- 🐛 Bug? → Bug report
- ✨ Feature idea? → Feature request
- 🔒 Security? → Private disclosure — never post a vulnerability publicly.
English and French both welcome.
- WaveFlow — the desktop player, a separate project that consumes this API for multi-device sync.
WaveFlow Server is licensed under AGPL-3.0-only: it hosts a network service, so anyone you serve it to is entitled to its source. The desktop application and waveflow-core are GPL-3.0-only.