“Why did Spotify increase their price again …. How does Spotify work …. LibreBeats …” — how this project came to be.
LibreBeats is a self-hosted music streaming platform: a Spotify-style client you run yourself, backed by your own catalog and infrastructure instead of a commercial subscription.
The project is split in two:
src/frontend: cross platform Flutter app (Home, Search, Library, Liked, Settings + mini/full player UI)src/backend: self-hosted Supabase plus Go services for SQL migrations and audio ingest
Current state: the app streams real audio from one or more self hosted Supabase backends. Search, beatmix browsing, background playback and multi-server management (QR code add in Settings, 20 minute catalog cache) all work. Home shows your last 10 played beats (persisted), Library and Liked still run on sample data. On the backend a queue worker ingests YouTube URLs into Postgres and Supabase Storage. More frontend detail in src/frontend/README.md.
- Flutter SDK, Dart
>=3.0.0 <4.0.0 - Docker with Docker Compose for the Supabase stack
- Go
1.25+for the migration and audio services - Bash for the
src/backend/*.shhelper scripts (Linux/macOS/WSL)
┌─────────────────────────────────────────────────────────────┐
│ Flutter app (src/frontend) │
│ Home · Search · Library · Liked · Settings · player │
│ Provider state · streaming playback · multi-server catalog │
└───────────────────────────┬─────────────────────────────────┘
│ Working: Supabase auth (no register, existing accounts only) +
│ real audio (search, playback, background) against one or more servers
▼
┌─────────────────────────────────────────────────────────────┐
│ Self-hosted Supabase (src/backend/supabase), 1..n of these │
│ Auth · PostgREST · Storage · Realtime · Studio · Kong │
└───────────────────────────┬─────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
PostgreSQL Supabase Storage PGMQ queue
Librebeats schema (audio + thumbs) audiopipe-input
▲ ▲ │
│ │ ▼
│ │ Go audio service (yt-dlp)
│ └──────────────────┘
│
Go migration service (SQL scripts on startup)
- Ingest: URLs get enqueued on
audiopipe-input, the Go audio worker downloads them with yt-dlp, uploads to Storage and writes the catalog rows. - Catalog: clients fetch
BeatMixrecords (with their beats embedded) from every registered server via themenuedge function, merged and cached in the app. - Play: the app streams straight from the catalog urls (offline cache still planned).
- Personal library: user playlists and external music servers. Multiple LibreBeats servers can be added in settings with a QR scan, Navidrome/Jellyfin not started.
LibreBeats/
├── README.md
└── src/
├── frontend/
│ ├── README.md # Frontend docs: architecture, config, testing
│ ├── lib/
│ │ ├── main.dart # Entry point: server seed, audio service, providers
│ │ ├── app.dart # MaterialApp + Material 3 dark theme
│ │ ├── config/ # helpers
│ │ ├── models/ # Beat/BeatMix/SearchResult models + sample data
│ │ ├── providers/ # BackgroundAudioProvider, LibreProvider (catalog + cache)
│ │ ├── services/ # AudioPlaybackService (just_audio + audio_service)
│ │ ├── data/ # ServerRegistry, repositories, disk stores (cache, history)
│ │ ├── screens/ # main_scaffold, home, search, library, liked, settings
│ │ └── widgets/ # mini/full player, tiles, QR server scanner
│ └── test/ # Flutter unit + widget tests
└── backend/
├── README.md # Backend docs: scripts, services, schema, quirks
├── build.sh # Build Supabase + custom images
├── run.sh # Start stack
├── stop.sh # docker compose down
├── variables.sh # PROJECT_DIR, BUILD_DIRECTORY, …
└── supabase/
├── docker-compose.yml
└── service/
├── migration/
│ ├── migration.go
│ ├── migration_helpers.go
│ ├── migration_test.go
│ └── scripts/
│ └── 0 initial.sql
└── audio/
├── main.go # Queue consumer loop
├── pipeline.go # Queue message / URL helpers
├── database.go # Beat catalog writes
├── queue.go # PGMQ pop
├── storage.go # Supabase Storage uploads
├── sourceHelper.go # yt-dlp integration
└── *_test.go # Unit tests
Flutter app with a dark, Spotify-like shell.
| Screen | Purpose |
|---|---|
| Home | Greeting + horizontal history row (last 10 played, persisted) |
| Search | Title search + "Browse Playlists" grid merging beatmixes from all registered servers |
| Library | Filter chips, Liked Songs entry, playlist list (sample data) |
| Liked | Liked Songs hero header + track list (sample data) |
| Settings | Server management (add via QR scan, status dots, remove) + about |
Most declared packages are in real use by now: provider, supabase_flutter, just_audio + audio_service + audio_session, cached_network_image, mobile_scanner, shared_preferences and google_fonts. Only path_provider is still waiting for the download feature, see pubspec.yaml.
Where it stands: real streaming (single beats and beatmix queues) with media notifications and background playback, playback pauses when headphones unplug or a call comes in. Servers are added via QR code in settings and persisted on device, the search grid merges every server's beatmixes and caches them for 20 minutes, Home keeps a persistent history of the last 10 plays. Library and Liked still run on sample data. Full details, config and the feature table live in src/frontend/README.md.
More backend detail in src/backend/README.md: the deploy scripts, how the build directory works, both services and their quirks.
src/backend/supabase is the official self-hosted Supabase Docker Compose setup. See Self-Hosting with Docker.
- Configure via
.env(copied from.env.exampleon firstbuild.sh). variables.shsetsPROJECT_DIR,BUILD_DIRECTORY, andGENERATE_KEYS(runsutils/generate-keys.shwhentrue).
| Service | Package path | Responsibility |
|---|---|---|
| migration | service/migration |
Applies numbered SQL in scripts/; records runs in Librebeats.Migrations |
| audio | service/audio |
Consumes audiopipe-input (PGMQ), runs yt-dlp, uploads to Storage, writes Beat / BeatMix |
Defined in 0 initial.sql:
| Table | Purpose | Client access |
|---|---|---|
RawBeat |
Staging: source URL, storage keys, duration | Service role only |
Beat |
Published track metadata + streaming URLs | Authenticated SELECT |
BeatMix |
Playlist / mix metadata | Authenticated SELECT |
BeatMixBeat |
Beat ↔ mix junction | Authenticated SELECT |
AudioOutputLog |
Ingest / processing log | Service role only |
Migrations |
Applied migration tracking | Service role only |
PGMQ queues: audiopipe-input (work), audiopipe-dlq (failed jobs) — message shape { "url": "https://..." }. The audio worker uses visibility timeout (pgmq.read); messages are only deleted on success (pgmq.delete). Transient failures retry until QUEUE_MAX_READ_COUNT; poison or exhausted messages move to the DLQ.
- Poll PGMQ for a URL message.
- Detect single video vs playlist (
playlist?in URL). - Download with yt-dlp (Opus + thumbnails).
- Upload to Supabase Storage buckets.
- Insert
RawBeat→Beat; for playlists,BeatMix+BeatMixBeat.
Both the Go services and the Flutter app have unit tests, none of them need Docker, Postgres, yt-dlp or a device.
# Migration helpers + script naming
cd src/backend/supabase/service/migration
go test ./...
# Pipeline parsing, file utilities, env guards, models
cd src/backend/supabase/service/audio
go test ./...
# Frontend unit + widget tests
cd src/frontend
flutter test| Package | What is tested |
|---|---|
migration |
Migration filename ID parsing, “migrations table missing” detection, scripts/ naming convention |
audio |
Queue JSON URL parsing, playlist URL detection, directory/file helpers, archive lookup, ProgressState, required env panics |
frontend |
Models, ServerRegistry (persistence/reconnect), catalog provider (merge/drip/cache/failures), disk stores (catalog cache, play history), QR payload parsing, add-server dialog, BeatTile, settings servers card |
Integration tests against a live Supabase stack are not included yet.
From src/backend. Edit variables.sh if needed (default build output: ~/librebeats/Herman).
./build.sh # Copy compose tree, build migration image (+ optional key generation)
./run.sh # Build audio image, docker compose up -d
./stop.sh # docker compose downAfter startup, use Studio and API URLs from your .env / SUPABASE_PUBLIC_URL.
Production: Default Supabase self-host settings are not production-safe. Rotate secrets, review CORS, and read security notes before exposing the stack.
cd src/frontend
flutter pub get
flutter run \
--dart-define=LIBREBEATS_SEED_URLS=https://your-server.example.com \
--dart-define=LIBREBEATS_SEED_KEYS=sb_publishable_yourkeyThe dart-defines hold the first-run server seed. No login ships in the app: the sign-in account is set up in settings on first run (a default login plus per-server overrides), can arrive via a QR scan, or can be baked into local dev builds with a git-ignored env.json. More in src/frontend/README.md.
| Area | Target | Current |
|---|---|---|
| Playback | Stream from Storage / signed URLs | Done, with background playback + media notification |
| Catalog | Read Beat / BeatMix from Supabase |
Done, menu edge function merged across servers with a 20 min cache |
| Auth | Supabase Auth in Flutter | Wired for existing users, can't register (jet) |
| Music servers | LibreBeats / Navidrome / Jellyfin | Multiple LibreBeats servers via QR in settings, Navidrome/Jellyfin not started |
| Ingest | Queue YouTube URLs → catalog | Worker with VT + DLQ; app not connected |
| Tests | CI + integration tests for DB/queue | Go + Flutter unit/widget tests |
Audio worker env (optional): QUEUE_VISIBILITY_TIMEOUT_SEC (default 600), QUEUE_MAX_READ_COUNT (default 5), QUEUE_DLQ_NAME (default audiopipe-dlq). Container restart: unless-stopped.
- Supabase self-host files: upstream licensing and docs in
src/backend/supabase/README.md. - Other components (Flutter, yt-dlp, Go modules, etc.) carry their own licenses, check each dependency before distribution.