A local music player rebuilt with TanStack Start, TanStack Router, TanStack Query, Drizzle, and PostgreSQL.
This app plays audio from a local tracks/ folder, stores music and playlist data in Postgres, serves album/playlist artwork from a local covers/ folder, and provides a desktop-style library UI for browsing, searching, playback, and playlist management.
Start the full app stack with Docker Compose:
docker compose up -dOpen http://localhost:3000.
The default Compose stack builds the production app image, starts PostgreSQL, runs database migrations, and mounts local music/artwork folders from the project root:
tracks/: local audio filescovers/: extracted and uploaded cover images
PostgreSQL is available from the host at:
postgresql://postgres:postgres@localhost:54322/musicDocker images default to a Huawei Cloud SWR mirror to avoid Docker Hub pull
timeouts. Override them with NODE_IMAGE or POSTGRES_IMAGE if your network
uses a different mirror. Docker builds also default to registry.npmmirror.com;
override it with NPM_CONFIG_REGISTRY when needed. The default platform is
linux/amd64 for mirror compatibility; override DOCKER_PLATFORM if your
mirror provides another architecture.
View service status and logs:
docker compose ps
docker compose logs appStop the stack:
docker compose downAdd audio files to a top-level tracks/ folder:
mkdir -p tracksSupported seed formats:
.mp3.flac
One way to download a YouTube playlist into local files is yt-dlp:
yt-dlp -x --audio-format mp3 --add-metadata --embed-thumbnail "https://www.youtube.com/playlist?list=..."Embedded cover art is extracted during seeding and written to covers/.
Docker runs migrations automatically when the app container starts. To import
music from tracks/, run the seed command manually after the stack is up:
docker compose exec app pnpm db:seedThe seed command clears existing songs and playlists before scanning tracks/,
so only run it when you want to rebuild the library from local files.
For non-Docker local development, install dependencies:
pnpm installThen create a .env file:
POSTGRES_URL=postgresql://postgres:postgres@localhost:54322/musicRun the local dev server:
pnpm db:migrate
pnpm devOpen Drizzle Studio:
pnpm db:studioUseful database commands:
pnpm db:generate: generate migrations fromsrc/lib/db/schema.tspnpm db:migrate: run migrations and enablepg_trgmfor fuzzy searchpnpm db:seed: clear songs/playlists, then scantracks/and insert songspnpm db:enhance: enhance existing metadatapnpm db:truncate: clear song and playlist datapnpm db:push: push schema changes directlypnpm db:drop: drop Drizzle-managed database objects
If POSTGRES_URL is not set, the app still starts with empty library states.
docker compose up -dOpen http://localhost:3000.
Build the app:
pnpm buildRun the built Nitro server:
pnpm previewThe production server entry is .output/server/index.mjs.
- Local music library backed by PostgreSQL
- File-based routing with TanStack Router
- Server functions and API routes with TanStack Start
- TanStack Query for playlists, songs, mutations, and cache refresh
- MediaSession API support for system playback controls
- All tracks view
- Playlist detail pages
- Create, rename, delete, and browse playlists
- Add tracks to playlists
- Upload playlist covers to local
covers/ - Serve audio from local
tracks/ - Basic search across tracks, artists, and albums
- Keyboard navigation for track list and sidebar
- Spacebar play/pause shortcut
- Progress bar seeking
- Volume controls
- Mobile library drawer
- Empty states for no music and no playlists
src/routes/ File-based routes and API routes
src/components/ Player, playlist, dialog, and table components
src/hooks/ Playback and playlist hooks
src/lib/db/ Drizzle schema, queries, migrations, and seed scripts
src/lib/server/ TanStack Start server functions
tracks/ Local audio files, git ignored
covers/ Local cover images, git ignored- This project does not use Vercel Blob.
- Local audio is served through
/api/audio/$filename. - Local cover images are served through
/api/cover/$filename. - Search uses Postgres
pg_trgm, which is enabled duringpnpm db:migrate.