music on atproto
plyr.fm
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### December 2025
49
50#### confidential OAuth client (PRs #578, #580-582, Dec 12-13)
51
52**confidential client support** (PR #578):
53- implemented ATProto OAuth confidential client using `private_key_jwt` authentication
54- when `OAUTH_JWK` is configured, plyr.fm authenticates with a cryptographic key
55- confidential clients earn 180-day refresh tokens (vs 2-week for public clients)
56- added `/.well-known/jwks.json` endpoint for public key discovery
57- updated `/oauth-client-metadata.json` with confidential client fields
58
59**bug fixes** (PRs #580-582):
60- fixed client assertion JWT to use Authorization Server's issuer as `aud` claim (not token endpoint URL)
61- fixed JWKS endpoint to preserve `kid` field from original JWK
62- fixed `OAuthClient` to pass `client_secret_kid` for JWT header
63
64**atproto fork updates** (zzstoatzz/atproto#6, #7):
65- added `issuer` parameter to `_make_token_request()` for correct `aud` claim
66- added `client_secret_kid` parameter to include `kid` in client assertion JWT header
67
68**outcome**: users now get 180-day refresh tokens, and "remember this account" on the PDS authorization page works (auto-approves subsequent logins). see #583 for future work on account switching via OAuth `prompt` parameter.
69
70---
71
72#### pagination & album management (PRs #550-554, Dec 9-10)
73
74**tracks list pagination** (PR #554):
75- cursor-based pagination on `/tracks/` endpoint (default 50 per page)
76- infinite scroll on homepage using native IntersectionObserver
77- zero new dependencies - uses browser APIs only
78- pagination state persisted to localStorage for fast subsequent loads
79
80**album management improvements** (PRs #550-552):
81- album delete and track reorder fixes
82- album page edit mode matching playlist UX (inline title editing, cover upload)
83- optimistic UI updates for album title changes (instant feedback)
84- ATProto record sync when album title changes (updates all track records + list record)
85
86**playlist show on profile** (PR #553):
87- restored "show on profile" toggle that was lost during inline editing refactor
88- users can now control whether playlists appear on their public profile
89
90---
91
92#### public cost dashboard (PR #548, Dec 9)
93
94- `/costs` page showing live platform infrastructure costs
95- daily export to R2 via GitHub Action, proxied through `/stats/costs` endpoint
96- includes fly.io, neon, cloudflare, and audd API costs
97- ko-fi integration for community support
98
99#### docket background tasks & concurrent exports (PRs #534-546, Dec 9)
100
101**docket integration** (PRs #534, #536, #539):
102- migrated background tasks from inline asyncio to docket (Redis-backed task queue)
103- copyright scanning, media export, ATProto sync, and teal scrobbling now run via docket
104- graceful fallback to asyncio for local development without Redis
105- parallel test execution with xdist template databases (#540)
106
107**concurrent export downloads** (PR #545):
108- exports now download tracks in parallel (up to 4 concurrent) instead of sequentially
109- significantly faster for users with many tracks or large files
110- zip creation remains sequential (zipfile constraint)
111
112**ATProto refactor** (PR #534):
113- reorganized ATProto record code into `_internal/atproto/records/` by lexicon namespace
114- extracted `client.py` for low-level PDS operations
115- cleaner separation between plyr.fm and teal.fm lexicons
116
117**documentation & observability**:
118- AudD API cost tracking dashboard (#546)
119- promoted runbooks from sandbox to `docs/runbooks/`
120- updated CLAUDE.md files across the codebase
121
122---
123
124#### artist support links & inline playlist editing (PRs #520-532, Dec 8)
125
126**artist support link** (PR #532):
127- artists can set a support URL (Ko-fi, Patreon, etc.) in their portal profile
128- support link displays as a button on artist profile pages next to the share button
129- URLs validated to require https:// prefix
130
131**inline playlist editing** (PR #531):
132- edit playlist name and description directly on playlist detail page
133- click-to-upload cover art replacement without modal
134- cleaner UX - no more edit modal popup
135
136**platform stats enhancements** (PRs #522, #528):
137- total duration displayed in platform stats (e.g., "42h 15m of music")
138- duration shown per artist in analytics section
139- combined stats and search into single centered container for cleaner layout
140
141**navigation & data loading fixes** (PR #527):
142- fixed stale data when navigating between detail pages of the same type
143- e.g., clicking from one artist to another now properly reloads data
144
145**copyright moderation improvements** (PR #480):
146- enhanced moderation workflow for copyright claims
147- improved labeler integration
148
149**letta-backed status maintenance** (PR #529):
150- automated status maintenance using Letta AI agent
151- agent reviews merged PRs and updates STATUS.md narratively
152
153---
154
155#### playlist fast-follow fixes (PRs #507-519, Dec 7-8)
156
157**public playlist viewing** (PR #519):
158- playlists now publicly viewable without authentication
159- ATProto records are public by design - auth was unnecessary for read access
160- shared playlist URLs no longer redirect unauthenticated users to homepage
161
162**inline playlist creation** (PR #510):
163- clicking "create new playlist" from AddToMenu previously navigated to `/library?create=playlist`
164- this caused SvelteKit to reinitialize the layout, destroying the audio element and stopping playback
165- fix: added inline create form that creates playlist and adds track in one action without navigation
166
167**UI polish** (PRs #507-509, #515):
168- include `image_url` in playlist SSR data for og:image link previews
169- invalidate layout data after token exchange - fixes stale auth state after login
170- fixed stopPropagation blocking "create new playlist" link clicks
171- detail page button layouts: all buttons visible on mobile, centered AddToMenu on track detail
172- AddToMenu smart positioning: menu opens upward when near viewport bottom
173
174**documentation** (PR #514):
175- added lexicons overview documentation at `docs/lexicons/overview.md`
176- covers `fm.plyr.track`, `fm.plyr.like`, `fm.plyr.comment`, `fm.plyr.list`, `fm.plyr.actor.profile`
177
178---
179
180#### playlists, ATProto sync, and library hub (PR #499, Dec 6-7)
181
182**playlists** (full CRUD):
183- create, rename, delete playlists with cover art upload
184- add/remove/reorder tracks with drag-and-drop
185- playlist detail page with edit modal
186- "add to playlist" menu on tracks with inline create
187- playlist sharing with OpenGraph link previews
188
189**ATProto integration**:
190- `fm.plyr.list` lexicon for syncing playlists/albums to user PDSes
191- `fm.plyr.actor.profile` lexicon for artist profiles
192- automatic sync of albums, liked tracks, profile on login
193
194**library hub** (`/library`):
195- unified page with tabs: liked, playlists, albums
196- nav changed from "liked" → "library"
197
198**related**: scope upgrade OAuth flow (PR #503), settings consolidation (PR #496)
199
200---
201
202#### sensitive image moderation (PRs #471-488, Dec 5-6)
203
204- `sensitive_images` table flags problematic images
205- `show_sensitive_artwork` user preference
206- flagged images blurred everywhere: track lists, player, artist pages, search, embeds
207- Media Session API respects sensitive preference
208- SSR-safe filtering for og:image link previews
209
210---
211
212#### teal.fm scrobbling (PR #467, Dec 4)
213
214- native scrobbling to user's PDS using teal's ATProto lexicons
215- scrobble at 30% or 30 seconds (same threshold as play counts)
216- toggle in settings, link to pdsls.dev to view records
217
218---
219
220### Earlier December / November 2025
221
222See `.status_history/2025-12.md` and `.status_history/2025-11.md` for detailed history including:
223- unified search with Cmd+K (PR #447)
224- light/dark theme system (PR #441)
225- tag filtering and bufo easter egg (PRs #431-438)
226- developer tokens (PR #367)
227- copyright moderation system (PRs #382-395)
228- export & upload reliability (PRs #337-344)
229- transcoder API deployment (PR #156)
230
231## immediate priorities
232
233### known issues
234- playback auto-start on refresh (#225)
235- iOS PWA audio may hang on first play after backgrounding
236
237### immediate focus
238- **moderation cleanup**: consolidate copyright detection, reduce AudD API costs, streamline labeler integration (issues #541-544)
239
240### feature ideas
241- issue #334: add 'share to bluesky' option for tracks
242- issue #373: lyrics field and Genius-style annotations
243
244### backlog
245- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred
246
247## technical state
248
249### architecture
250
251**backend**
252- language: Python 3.11+
253- framework: FastAPI with uvicorn
254- database: Neon PostgreSQL (serverless)
255- storage: Cloudflare R2 (S3-compatible)
256- background tasks: docket (Redis-backed)
257- hosting: Fly.io (2x shared-cpu VMs)
258- observability: Pydantic Logfire
259- auth: ATProto OAuth 2.1
260
261**frontend**
262- framework: SvelteKit (v2.43.2)
263- runtime: Bun
264- hosting: Cloudflare Pages
265- styling: vanilla CSS with lowercase aesthetic
266- state management: Svelte 5 runes
267
268**deployment**
269- ci/cd: GitHub Actions
270- backend: automatic on main branch merge (fly.io)
271- frontend: automatic on every push to main (cloudflare pages)
272- migrations: automated via fly.io release_command
273
274**what's working**
275
276**core functionality**
277- ✅ ATProto OAuth 2.1 authentication
278- ✅ secure session management via HttpOnly cookies
279- ✅ developer tokens with independent OAuth grants
280- ✅ platform stats and Media Session API
281- ✅ timed comments with clickable timestamps
282- ✅ artist profiles synced with Bluesky
283- ✅ track upload with streaming
284- ✅ audio streaming via 307 redirects to R2 CDN
285- ✅ play count tracking, likes, queue management
286- ✅ unified search with Cmd/Ctrl+K
287- ✅ teal.fm scrobbling
288- ✅ copyright moderation with ATProto labeler
289- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble)
290- ✅ media export with concurrent downloads
291
292**albums**
293- ✅ album CRUD with cover art
294- ✅ ATProto list records (auto-synced on login)
295
296**playlists**
297- ✅ full CRUD with drag-and-drop reordering
298- ✅ ATProto list records (synced on create/modify)
299- ✅ "add to playlist" menu, global search results
300
301**deployment URLs**
302- production frontend: https://plyr.fm
303- production backend: https://api.plyr.fm
304- staging: https://stg.plyr.fm / https://api-stg.plyr.fm
305
306### technical decisions
307
308**why Python/FastAPI instead of Rust?**
309- rapid prototyping velocity during MVP phase
310- trade-off: accepting higher latency for faster development
311
312**why Cloudflare R2 instead of S3?**
313- zero egress fees (critical for audio streaming)
314- S3-compatible API, integrated CDN
315
316**why async everywhere?**
317- I/O-bound workload: most time spent waiting on network/disk
318- PRs #149-151 eliminated all blocking operations
319
320## cost structure
321
322current monthly costs: ~$18/month (plyr.fm specific)
323
324see live dashboard: [plyr.fm/costs](https://plyr.fm/costs)
325
326- fly.io (plyr apps only): ~$12/month
327 - relay-api (prod): $5.80
328 - relay-api-staging: $5.60
329 - plyr-moderation: $0.24
330 - plyr-transcoder: $0.02
331- neon postgres: $5/month
332- cloudflare (R2 + pages + domain): ~$1.16/month
333- audd audio fingerprinting: $0-10/month (6000 free/month)
334- logfire: $0 (free tier)
335
336## admin tooling
337
338### content moderation
339script: `scripts/delete_track.py`
340
341usage:
342```bash
343uv run scripts/delete_track.py <track_id> --dry-run
344uv run scripts/delete_track.py <track_id>
345uv run scripts/delete_track.py --url https://plyr.fm/track/34
346```
347
348## for new contributors
349
350### getting started
3511. clone: `gh repo clone zzstoatzz/plyr.fm`
3522. install dependencies: `uv sync && cd frontend && bun install`
3533. run backend: `uv run uvicorn backend.main:app --reload`
3544. run frontend: `cd frontend && bun run dev`
3555. visit http://localhost:5173
356
357### development workflow
3581. create issue on github
3592. create PR from feature branch
3603. ensure pre-commit hooks pass
3614. merge to main → deploys to staging
3625. create github release → deploys to production
363
364### key principles
365- type hints everywhere
366- lowercase aesthetic
367- ATProto first
368- async everywhere (no blocking I/O)
369- mobile matters
370- cost conscious
371
372### project structure
373```
374plyr.fm/
375├── backend/ # FastAPI app & Python tooling
376│ ├── src/backend/ # application code
377│ ├── tests/ # pytest suite
378│ └── alembic/ # database migrations
379├── frontend/ # SvelteKit app
380│ ├── src/lib/ # components & state
381│ └── src/routes/ # pages
382├── moderation/ # Rust moderation service (ATProto labeler)
383├── transcoder/ # Rust audio transcoding service
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 2025-12-13.