1# plyr.fm - status 2 3## long-term vision 4 5### the problem 6 7today's music streaming is fundamentally broken: 8- spotify and apple music trap your data in proprietary silos 9- artists pay distribution fees and streaming cuts to multiple gatekeepers 10- listeners can't own their music collections - they rent them 11- switching platforms means losing everything: playlists, play history, social connections 12 13### the atproto solution 14 15plyr.fm is built on the AT Protocol (the protocol powering Bluesky) and enables: 16- **portable identity**: your music collection, playlists, and listening history belong to you, stored in your personal data server (PDS) 17- **decentralized distribution**: artists publish directly to the network without platform gatekeepers 18- **interoperable data**: any client can read your music records - you're not locked into plyr.fm 19- **authentic social**: artist profiles are real ATProto identities with verifiable handles (@artist.bsky.social) 20 21### the dream state 22 23plyr.fm should become: 24 251. **for artists**: the easiest way to publish music to the decentralized web 26 - upload once, available everywhere in the ATProto network 27 - direct connection to listeners without platform intermediaries 28 - real ownership of audience relationships 29 302. **for listeners**: a streaming platform where you actually own your data 31 - your collection lives in your PDS, playable by any ATProto music client 32 - switch between plyr.fm and other clients freely - your data travels with you 33 - share tracks as native ATProto posts to Bluesky 34 353. **for developers**: a reference implementation showing how to build on ATProto 36 - open source end-to-end example of ATProto integration 37 - demonstrates OAuth, record creation, federation patterns 38 - proves decentralized music streaming is viable 39 40--- 41 42**started**: October 28, 2025 (first commit: `454e9bc` - relay MVP with ATProto authentication) 43 44--- 45 46## recent work 47 48### January 2026 49 50#### multi-account experience (PRs #707, #710, #712-714, Jan 3-5) 51 52**why**: many users have multiple ATProto identities (personal, artist, label). forcing re-authentication to switch was friction that discouraged uploads from secondary accounts. 53 54**users can now link multiple identities** to a single browser session: 55- add additional accounts via "add account" in user menu (triggers OAuth with `prompt=login`) 56- switch between linked accounts instantly without re-authenticating 57- logout from individual accounts or all at once 58- updated `/auth/me` returns `linked_accounts` array with avatars 59 60**backend changes**: 61- new `group_id` column on `user_sessions` links accounts together 62- new `pending_add_accounts` table tracks in-progress OAuth flows 63- new endpoints: `POST /auth/add-account/start`, `POST /auth/switch-account`, `POST /auth/logout-all` 64 65**infrastructure fixes** (PRs #710, #712, #714): 66these fixes came from reviewing [Bluesky's architecture deep dive](https://newsletter.pragmaticengineer.com/p/bluesky) which highlighted connection/resource management as scaling concerns. applied learnings to our own codebase: 67- identified Neon serverless connection overhead (~77ms per connection) via Logfire 68- cached `async_sessionmaker` per engine instead of recreating on every request (PR #712) 69- changed `_refresh_locks` from unbounded dict to LRUCache (10k max, 1hr TTL) to prevent memory leak (PR #710) 70- pass db session through auth helpers to reduce connections per request (PR #714) 71- result: `/auth/switch-account` ~1100ms → ~800ms, `/auth/me` ~940ms → ~720ms 72 73**frontend changes**: 74- UserMenu (desktop): collapsible accounts submenu with linked accounts, add account, logout all 75- ProfileMenu (mobile): dedicated accounts panel with avatars 76- fixed `invalidateAll()` not refreshing client-side loaded data by using `window.location.reload()` (PR #713) 77 78**docs**: [research/2026-01-03-multi-account-experience.md](docs/research/2026-01-03-multi-account-experience.md) 79 80--- 81 82#### artist bio links (PRs #700-701, Jan 2) 83 84**links in artist bios now render as clickable** - supports full URLs and bare domains (e.g., "example.com"): 85- regex extracts URLs from bio text 86- bare domain/path URLs handled correctly 87- links open in new tab 88 89--- 90 91#### copyright moderation improvements (PRs #703-704, Jan 2-3) 92 93**per legal advice**, redesigned copyright handling to reduce liability exposure: 94- **disabled auto-labeling** (PR #703): labels are no longer automatically emitted when copyright matches are detected. the system now only flags and notifies, leaving takedown decisions to humans 95- **raised threshold** (PR #703): copyright flag threshold increased from "any match" to configurable score (default 85%). controlled via `MODERATION_COPYRIGHT_SCORE_THRESHOLD` env var 96- **DM notifications** (PR #704): when a track is flagged, both the artist and admin receive BlueSky DMs with details. includes structured error handling for when users have DMs disabled 97- **observability** (PR #704): Logfire spans added to all notification paths (`send_dm`, `copyright_notification`) with error categorization (`dm_blocked`, `network`, `auth`, `unknown`) 98- **notification tracking**: `notified_at` field added to `copyright_scans` table to track which flags have been communicated 99 100**why this matters**: DMCA safe harbor requires taking action on notices, not proactively policing. auto-labeling was creating liability by making assertions about copyright status. human review is now required before any takedown action. 101 102--- 103 104#### ATProto OAuth permission sets (PRs #697-698, Jan 1-2) 105 106**permission sets enabled** - OAuth now uses `include:fm.plyr.authFullApp` instead of listing individual `repo:` scopes: 107- users see clean "plyr.fm" permission title instead of raw collection names 108- permission set lexicon published to `com.atproto.lexicon.schema` on plyr.fm authority repo 109- DNS TXT records at `_lexicon.plyr.fm` and `_lexicon.stg.plyr.fm` link namespaces to authority DID 110- fixed scope validation in atproto SDK fork to handle PDS permission expansion (`include:``repo?collection=`) 111 112**why this matters**: permission sets are ATProto's mechanism for defining platform access tiers. enables future third-party integrations (mobile apps, read-only stats dashboards) to request semantic permission bundles instead of raw collection lists. 113 114**docs**: [lexicons/overview.md](docs/lexicons/overview.md), [research/2026-01-01-atproto-oauth-permission-sets.md](docs/research/2026-01-01-atproto-oauth-permission-sets.md) 115 116--- 117 118#### atprotofans supporters display (PRs #695-696, Jan 1) 119 120**supporters now visible on artist pages** - artists using atprotofans can show their supporters: 121- compact overlapping avatar circles (GitHub sponsors style) with "+N" overflow badge 122- clicks link to supporter's plyr.fm artist page (keeps users in-app) 123- `POST /artists/batch` endpoint enriches supporter DIDs with avatar_url from our Artist table 124- frontend fetches from atprotofans, enriches via backend, renders with consistent avatar pattern 125 126**route ordering fix** (PR #696): FastAPI was matching `/artists/batch` as `/{did}` with did="batch". moved POST route before the catchall GET route. 127 128--- 129 130#### UI polish (PRs #692-694, Dec 31 - Jan 1) 131 132- **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views 133- **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls 134- **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules 135 136--- 137 138### December 2025 139 140See `.status_history/2025-12.md` for detailed history including: 141- header redesign and UI polish (PRs #691-693, Dec 31) 142- automated image moderation with Claude vision (PRs #687-690, Dec 31) 143- avatar sync on login (PR #685, Dec 31) 144- top tracks homepage (PR #684, Dec 31) 145- batch review system (PR #672, Dec 30) 146- CSS design tokens (PRs #662-664, Dec 29-30) 147- self-hosted redis migration (PRs #674-675, Dec 30) 148- supporter-gated content (PR #637, Dec 22-23) 149- supporter badges (PR #627, Dec 21-22) 150- end-of-year sprint: moderation + atprotofans (PRs #617-629, Dec 19-21) 151- offline mode foundation (PRs #610-611, Dec 17) 152- UX polish and login improvements (PRs #604-615, Dec 16-18) 153- visual customization with custom backgrounds (PRs #595-596, Dec 16) 154- performance & moderation polish (PRs #586-593, Dec 14-15) 155- mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 156- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13) 157- pagination & album management (PRs #550-554, Dec 9-10) 158- public cost dashboard (PRs #548-549, Dec 9) 159- docket background tasks & concurrent exports (PRs #534-546, Dec 9) 160- artist support links & inline playlist editing (PRs #520-532, Dec 8) 161- playlist fast-follow fixes (PRs #507-519, Dec 7-8) 162- playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 163- sensitive image moderation (PRs #471-488, Dec 5-6) 164- teal.fm scrobbling (PR #467, Dec 4) 165- unified search with Cmd+K (PR #447, Dec 3) 166- light/dark theme system (PR #441, Dec 2-3) 167- tag filtering and bufo easter egg (PRs #431-438, Dec 2) 168 169### November 2025 170 171See `.status_history/2025-11.md` for detailed history including: 172- developer tokens (PR #367) 173- copyright moderation system (PRs #382-395) 174- export & upload reliability (PRs #337-344) 175- transcoder API deployment (PR #156) 176 177## priorities 178 179### current focus 180 181stabilization and polish after multi-account release. monitoring production for issues. 182 183**end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) shipped:** 184- moderation consolidation: sensitive images moved to moderation service (#644) 185- moderation batch review UI with Claude vision integration (#672, #687-690) 186- atprotofans: supporter badges (#627) and content gating (#637) 187 188### known issues 189- playback auto-start on refresh (#225) 190- iOS PWA audio may hang on first play after backgrounding 191 192### backlog 193- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 194- share to bluesky (#334) 195- lyrics and annotations (#373) 196- configurable rules engine for moderation 197- time-release gating (#642) 198 199## technical state 200 201### architecture 202 203**backend** 204- language: Python 3.11+ 205- framework: FastAPI with uvicorn 206- database: Neon PostgreSQL (serverless) 207- storage: Cloudflare R2 (S3-compatible) 208- background tasks: docket (Redis-backed) 209- hosting: Fly.io (2x shared-cpu VMs) 210- observability: Pydantic Logfire 211- auth: ATProto OAuth 2.1 212 213**frontend** 214- framework: SvelteKit (v2.43.2) 215- runtime: Bun 216- hosting: Cloudflare Pages 217- styling: vanilla CSS with lowercase aesthetic 218- state management: Svelte 5 runes 219 220**deployment** 221- ci/cd: GitHub Actions 222- backend: automatic on main branch merge (fly.io) 223- frontend: automatic on every push to main (cloudflare pages) 224- migrations: automated via fly.io release_command 225 226**what's working** 227 228**core functionality** 229- ✅ ATProto OAuth 2.1 authentication 230- ✅ multi-account support (link multiple ATProto identities) 231- ✅ secure session management via HttpOnly cookies 232- ✅ developer tokens with independent OAuth grants 233- ✅ platform stats and Media Session API 234- ✅ timed comments with clickable timestamps 235- ✅ artist profiles synced with Bluesky 236- ✅ track upload with streaming 237- ✅ audio streaming via 307 redirects to R2 CDN 238- ✅ play count tracking, likes, queue management 239- ✅ unified search with Cmd/Ctrl+K 240- ✅ teal.fm scrobbling 241- ✅ copyright moderation with ATProto labeler 242- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 243- ✅ media export with concurrent downloads 244- ✅ supporter-gated content via atprotofans 245 246**albums** 247- ✅ album CRUD with cover art 248- ✅ ATProto list records (auto-synced on login) 249 250**playlists** 251- ✅ full CRUD with drag-and-drop reordering 252- ✅ ATProto list records (synced on create/modify) 253- ✅ "add to playlist" menu, global search results 254 255**deployment URLs** 256- production frontend: https://plyr.fm 257- production backend: https://api.plyr.fm 258- staging: https://stg.plyr.fm / https://api-stg.plyr.fm 259 260### technical decisions 261 262**why Python/FastAPI instead of Rust?** 263- rapid prototyping velocity during MVP phase 264- trade-off: accepting higher latency for faster development 265 266**why Cloudflare R2 instead of S3?** 267- zero egress fees (critical for audio streaming) 268- S3-compatible API, integrated CDN 269 270**why async everywhere?** 271- I/O-bound workload: most time spent waiting on network/disk 272- PRs #149-151 eliminated all blocking operations 273 274## cost structure 275 276current monthly costs: ~$20/month (plyr.fm specific) 277 278see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 279 280- fly.io (backend + redis + moderation): ~$14/month 281- neon postgres: $5/month 282- cloudflare (R2 + pages + domain): ~$1/month 283- audd audio fingerprinting: $5-10/month (usage-based) 284- logfire: $0 (free tier) 285 286## admin tooling 287 288### content moderation 289script: `scripts/delete_track.py` 290 291usage: 292```bash 293uv run scripts/delete_track.py <track_id> --dry-run 294uv run scripts/delete_track.py <track_id> 295uv run scripts/delete_track.py --url https://plyr.fm/track/34 296``` 297 298## for new contributors 299 300### getting started 3011. clone: `gh repo clone zzstoatzz/plyr.fm` 3022. install dependencies: `uv sync && cd frontend && bun install` 3033. run backend: `uv run uvicorn backend.main:app --reload` 3044. run frontend: `cd frontend && bun run dev` 3055. visit http://localhost:5173 306 307### development workflow 3081. create issue on github 3092. create PR from feature branch 3103. ensure pre-commit hooks pass 3114. merge to main → deploys to staging 3125. create github release → deploys to production 313 314### key principles 315- type hints everywhere 316- lowercase aesthetic 317- ATProto first 318- async everywhere (no blocking I/O) 319- mobile matters 320- cost conscious 321 322### project structure 323``` 324plyr.fm/ 325├── backend/ # FastAPI app & Python tooling 326│ ├── src/backend/ # application code 327│ ├── tests/ # pytest suite 328│ └── alembic/ # database migrations 329├── frontend/ # SvelteKit app 330│ ├── src/lib/ # components & state 331│ └── src/routes/ # pages 332├── moderation/ # Rust moderation service (ATProto labeler) 333├── transcoder/ # Rust audio transcoding service 334├── redis/ # self-hosted Redis config 335├── docs/ # documentation 336└── justfile # task runner 337``` 338 339## documentation 340 341- [docs/README.md](docs/README.md) - documentation index 342- [runbooks](docs/runbooks/) - production incident procedures 343- [background tasks](docs/backend/background-tasks.md) - docket task system 344- [logfire querying](docs/tools/logfire.md) - observability queries 345- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content 346- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas 347 348--- 349 350this is a living document. last updated 2026-01-06.