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#### ATProto OAuth permission sets (PRs #697-698, Jan 1-2) 51 52**permission sets enabled** - OAuth now uses `include:fm.plyr.authFullApp` instead of listing individual `repo:` scopes: 53- users see clean "plyr.fm" permission title instead of raw collection names 54- permission set lexicon published to `com.atproto.lexicon.schema` on plyr.fm authority repo 55- DNS TXT records at `_lexicon.plyr.fm` and `_lexicon.stg.plyr.fm` link namespaces to authority DID 56- fixed scope validation in atproto SDK fork to handle PDS permission expansion (`include:``repo?collection=`) 57 58**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. 59 60**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) 61 62--- 63 64#### atprotofans supporters display (PRs #695-696, Jan 1) 65 66**supporters now visible on artist pages** - artists using atprotofans can show their supporters: 67- compact overlapping avatar circles (GitHub sponsors style) with "+N" overflow badge 68- clicks link to supporter's plyr.fm artist page (keeps users in-app) 69- `POST /artists/batch` endpoint enriches supporter DIDs with avatar_url from our Artist table 70- frontend fetches from atprotofans, enriches via backend, renders with consistent avatar pattern 71 72**route ordering fix** (PR #696): FastAPI was matching `/artists/batch` as `/{did}` with did="batch". moved POST route before the catchall GET route. 73 74--- 75 76#### UI polish (PRs #692-694, Dec 31 - Jan 1) 77 78- **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views 79- **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls 80- **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules 81 82--- 83 84### December 2025 85 86#### avatar sync on login (PR #685, Dec 31) 87 88**avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: 89- on login, avatar is refreshed from Bluesky and synced to both postgres and ATProto profile record 90- added `avatar` field to `fm.plyr.actor.profile` lexicon (optional, URI format) 91- one-time backfill script (`scripts/backfill_avatars.py`) refreshed 28 stale avatars in production 92 93--- 94 95#### self-hosted redis (PR #674-675, Dec 30) 96 97**replaced Upstash with self-hosted Redis on Fly.io** - ~$75/month → ~$4/month: 98- Upstash pay-as-you-go was charging per command (37M commands = $75) 99- self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 100- deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 101- added CI workflow for redis deployments on merge 102 103**no state migration needed** - docket stores ephemeral task queue data, job progress lives in postgres. 104 105--- 106 107#### supporter-gated content (PR #637, Dec 22-23) 108 109**atprotofans paywall integration** - artists can now mark tracks as "supporters only": 110- tracks with `support_gate` require atprotofans validation before playback 111- non-supporters see lock icon and "become a supporter" CTA linking to atprotofans 112- artists can always play their own gated tracks 113 114**backend architecture**: 115- audio endpoint validates supporter status via atprotofans API before serving gated content 116- HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues) 117- `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 118- background task handles bucket migration asynchronously 119- ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`) 120 121**frontend**: 122- `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 123- clicking locked track shows toast with CTA - does NOT interrupt current playback 124- portal shows support gate toggle in track edit UI 125 126--- 127 128#### supporter badges (PR #627, Dec 21-22) 129 130**phase 1 of atprotofans integration**: 131- supporter badge displays on artist pages when logged-in viewer supports the artist 132- calls atprotofans `validateSupporter` API directly from frontend (public endpoint) 133- badge only shows when viewer is authenticated and not viewing their own profile 134 135--- 136 137#### rate limit moderation endpoint (PR #629, Dec 21) 138 139**incident response**: detected suspicious activity - 72 requests in 17 seconds from a single IP targeting `/moderation/sensitive-images`. added `10/minute` rate limit using existing slowapi infrastructure. 140 141--- 142 143#### end-of-year sprint planning (PR #626, Dec 20) 144 145**focus**: two foundational systems need solid experimental implementations by 2026. 146 147| track | focus | status | 148|-------|-------|--------| 149| moderation | consolidate architecture, add rules engine | in progress | 150| atprotofans | supporter validation, content gating | shipped (phase 1-3) | 151 152**research docs**: 153- [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 154- [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 155 156--- 157 158#### beartype + moderation cleanup (PRs #617-619, Dec 19) 159 160**runtime type checking** (PR #619): 161- enabled beartype runtime type validation across the backend 162- catches type errors at runtime instead of silently passing bad data 163- test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 164 165**moderation cleanup** (PRs #617-618): 166- consolidated moderation code, addressing issues #541-543 167- `sync_copyright_resolutions` now runs automatically via docket Perpetual task 168- removed dead `init_db()` from lifespan (handled by alembic migrations) 169 170--- 171 172#### UX polish (PRs #604-607, #613, #615, Dec 16-18) 173 174**login improvements** (PRs #604, #613): 175- login page now uses "internet handle" terminology for clarity 176- input normalization: strips `@` and `at://` prefixes automatically 177 178**artist page fixes** (PR #615): 179- track pagination on artist pages now works correctly 180- fixed mobile album card overflow 181 182**mobile + metadata** (PRs #605-607): 183- Open Graph tags added to tag detail pages for link previews 184- mobile modals now use full screen positioning 185- fixed `/tag/` routes in hasPageMetadata check 186 187--- 188 189#### offline mode foundation (PRs #610-611, Dec 17) 190 191**experimental offline playback**: 192- storage layer using Cache API for audio bytes + IndexedDB for metadata 193- `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 194- "auto-download liked" toggle in experimental settings section 195- Player checks for cached audio before streaming from R2 196 197--- 198 199### Earlier December 2025 200 201See `.status_history/2025-12.md` for detailed history including: 202- visual customization with custom backgrounds (PRs #595-596, Dec 16) 203- performance & moderation polish (PRs #586-593, Dec 14-15) 204- mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 205- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13) 206- pagination & album management (PRs #550-554, Dec 9-10) 207- public cost dashboard (PRs #548-549, Dec 9) 208- docket background tasks & concurrent exports (PRs #534-546, Dec 9) 209- artist support links & inline playlist editing (PRs #520-532, Dec 8) 210- playlist fast-follow fixes (PRs #507-519, Dec 7-8) 211- playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 212- sensitive image moderation (PRs #471-488, Dec 5-6) 213- teal.fm scrobbling (PR #467, Dec 4) 214- unified search with Cmd+K (PR #447, Dec 3) 215- light/dark theme system (PR #441, Dec 2-3) 216- tag filtering and bufo easter egg (PRs #431-438, Dec 2) 217 218### November 2025 219 220See `.status_history/2025-11.md` for detailed history including: 221- developer tokens (PR #367) 222- copyright moderation system (PRs #382-395) 223- export & upload reliability (PRs #337-344) 224- transcoder API deployment (PR #156) 225 226## immediate priorities 227 228### quality of life mode (Dec 29-31) 229 230end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) complete. remaining days before 2026 are for minor polish and bug fixes as they arise. 231 232**what shipped in the sprint:** 233- moderation consolidation: sensitive images moved to moderation service (#644) 234- atprotofans: supporter badges (#627) and content gating (#637) 235 236**aspirational (deferred until scale justifies):** 237- configurable rules engine for moderation 238- time-release gating (#642) 239 240### known issues 241- playback auto-start on refresh (#225) 242- iOS PWA audio may hang on first play after backgrounding 243 244### backlog 245- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 246- share to bluesky (#334) 247- lyrics and annotations (#373) 248 249## technical state 250 251### architecture 252 253**backend** 254- language: Python 3.11+ 255- framework: FastAPI with uvicorn 256- database: Neon PostgreSQL (serverless) 257- storage: Cloudflare R2 (S3-compatible) 258- background tasks: docket (Redis-backed) 259- hosting: Fly.io (2x shared-cpu VMs) 260- observability: Pydantic Logfire 261- auth: ATProto OAuth 2.1 262 263**frontend** 264- framework: SvelteKit (v2.43.2) 265- runtime: Bun 266- hosting: Cloudflare Pages 267- styling: vanilla CSS with lowercase aesthetic 268- state management: Svelte 5 runes 269 270**deployment** 271- ci/cd: GitHub Actions 272- backend: automatic on main branch merge (fly.io) 273- frontend: automatic on every push to main (cloudflare pages) 274- migrations: automated via fly.io release_command 275 276**what's working** 277 278**core functionality** 279- ✅ ATProto OAuth 2.1 authentication 280- ✅ secure session management via HttpOnly cookies 281- ✅ developer tokens with independent OAuth grants 282- ✅ platform stats and Media Session API 283- ✅ timed comments with clickable timestamps 284- ✅ artist profiles synced with Bluesky 285- ✅ track upload with streaming 286- ✅ audio streaming via 307 redirects to R2 CDN 287- ✅ play count tracking, likes, queue management 288- ✅ unified search with Cmd/Ctrl+K 289- ✅ teal.fm scrobbling 290- ✅ copyright moderation with ATProto labeler 291- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 292- ✅ media export with concurrent downloads 293- ✅ supporter-gated content via atprotofans 294 295**albums** 296- ✅ album CRUD with cover art 297- ✅ ATProto list records (auto-synced on login) 298 299**playlists** 300- ✅ full CRUD with drag-and-drop reordering 301- ✅ ATProto list records (synced on create/modify) 302- ✅ "add to playlist" menu, global search results 303 304**deployment URLs** 305- production frontend: https://plyr.fm 306- production backend: https://api.plyr.fm 307- staging: https://stg.plyr.fm / https://api-stg.plyr.fm 308 309### technical decisions 310 311**why Python/FastAPI instead of Rust?** 312- rapid prototyping velocity during MVP phase 313- trade-off: accepting higher latency for faster development 314 315**why Cloudflare R2 instead of S3?** 316- zero egress fees (critical for audio streaming) 317- S3-compatible API, integrated CDN 318 319**why async everywhere?** 320- I/O-bound workload: most time spent waiting on network/disk 321- PRs #149-151 eliminated all blocking operations 322 323## cost structure 324 325current monthly costs: ~$20/month (plyr.fm specific) 326 327see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 328 329- fly.io (backend + redis + moderation): ~$14/month 330- neon postgres: $5/month 331- cloudflare (R2 + pages + domain): ~$1/month 332- audd audio fingerprinting: $5-10/month (usage-based) 333- logfire: $0 (free tier) 334 335## admin tooling 336 337### content moderation 338script: `scripts/delete_track.py` 339 340usage: 341```bash 342uv run scripts/delete_track.py <track_id> --dry-run 343uv run scripts/delete_track.py <track_id> 344uv run scripts/delete_track.py --url https://plyr.fm/track/34 345``` 346 347## for new contributors 348 349### getting started 3501. clone: `gh repo clone zzstoatzz/plyr.fm` 3512. install dependencies: `uv sync && cd frontend && bun install` 3523. run backend: `uv run uvicorn backend.main:app --reload` 3534. run frontend: `cd frontend && bun run dev` 3545. visit http://localhost:5173 355 356### development workflow 3571. create issue on github 3582. create PR from feature branch 3593. ensure pre-commit hooks pass 3604. merge to main → deploys to staging 3615. create github release → deploys to production 362 363### key principles 364- type hints everywhere 365- lowercase aesthetic 366- ATProto first 367- async everywhere (no blocking I/O) 368- mobile matters 369- cost conscious 370 371### project structure 372``` 373plyr.fm/ 374├── backend/ # FastAPI app & Python tooling 375│ ├── src/backend/ # application code 376│ ├── tests/ # pytest suite 377│ └── alembic/ # database migrations 378├── frontend/ # SvelteKit app 379│ ├── src/lib/ # components & state 380│ └── src/routes/ # pages 381├── moderation/ # Rust moderation service (ATProto labeler) 382├── transcoder/ # Rust audio transcoding service 383├── redis/ # self-hosted Redis config 384├── docs/ # documentation 385└── justfile # task runner 386``` 387 388## documentation 389 390- [docs/README.md](docs/README.md) - documentation index 391- [runbooks](docs/runbooks/) - production incident procedures 392- [background tasks](docs/backend/background-tasks.md) - docket task system 393- [logfire querying](docs/tools/logfire.md) - observability queries 394- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content 395- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas 396 397--- 398 399this is a living document. last updated 2026-01-02.