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### 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#### auth stabilization (PRs #734-736, Jan 6-7)
83
84**why**: multi-account support introduced edge cases where auth state could become inconsistent between frontend components, and sessions could outlive their refresh tokens.
85
86**session expiry alignment** (PR #734):
87- sessions now track refresh token lifetime and respect it during validation
88- prevents sessions from appearing valid after their underlying OAuth grant expires
89- dev token expiration handling aligned with same pattern
90
91**queue auth boundary fix** (PR #735):
92- queue component now uses shared layout auth state instead of localStorage session IDs
93- fixes race condition where queue could attempt authenticated requests before layout resolved auth
94- ensures remote queue snapshots don't inherit local update flags during hydration
95
96**playlist cover upload fix** (PR #736):
97- `R2Storage.save()` was rejecting `BytesIO` objects due to beartype's strict `BinaryIO` protocol checking
98- changed type hint to `BinaryIO | BytesIO` to explicitly accept both
99- found via Logfire: only 2 failures in production, both on Jan 3
100
101---
102
103#### artist bio links (PRs #700-701, Jan 2)
104
105**links in artist bios now render as clickable** - supports full URLs and bare domains (e.g., "example.com"):
106- regex extracts URLs from bio text
107- bare domain/path URLs handled correctly
108- links open in new tab
109
110---
111
112#### copyright moderation improvements (PRs #703-704, Jan 2-3)
113
114**per legal advice**, redesigned copyright handling to reduce liability exposure:
115- **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
116- **raised threshold** (PR #703): copyright flag threshold increased from "any match" to configurable score (default 85%). controlled via `MODERATION_COPYRIGHT_SCORE_THRESHOLD` env var
117- **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
118- **observability** (PR #704): Logfire spans added to all notification paths (`send_dm`, `copyright_notification`) with error categorization (`dm_blocked`, `network`, `auth`, `unknown`)
119- **notification tracking**: `notified_at` field added to `copyright_scans` table to track which flags have been communicated
120
121**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.
122
123---
124
125#### ATProto OAuth permission sets (PRs #697-698, Jan 1-2)
126
127**permission sets enabled** - OAuth now uses `include:fm.plyr.authFullApp` instead of listing individual `repo:` scopes:
128- users see clean "plyr.fm" permission title instead of raw collection names
129- permission set lexicon published to `com.atproto.lexicon.schema` on plyr.fm authority repo
130- DNS TXT records at `_lexicon.plyr.fm` and `_lexicon.stg.plyr.fm` link namespaces to authority DID
131- fixed scope validation in atproto SDK fork to handle PDS permission expansion (`include:` → `repo?collection=`)
132
133**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.
134
135**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)
136
137---
138
139#### atprotofans supporters display (PRs #695-696, Jan 1)
140
141**supporters now visible on artist pages** - artists using atprotofans can show their supporters:
142- compact overlapping avatar circles (GitHub sponsors style) with "+N" overflow badge
143- clicks link to supporter's plyr.fm artist page (keeps users in-app)
144- `POST /artists/batch` endpoint enriches supporter DIDs with avatar_url from our Artist table
145- frontend fetches from atprotofans, enriches via backend, renders with consistent avatar pattern
146
147**route ordering fix** (PR #696): FastAPI was matching `/artists/batch` as `/{did}` with did="batch". moved POST route before the catchall GET route.
148
149---
150
151#### UI polish (PRs #692-694, Dec 31 - Jan 1)
152
153- **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views
154- **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls
155- **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules
156
157---
158
159### December 2025
160
161See `.status_history/2025-12.md` for detailed history including:
162- header redesign and UI polish (PRs #691-693, Dec 31)
163- automated image moderation with Claude vision (PRs #687-690, Dec 31)
164- avatar sync on login (PR #685, Dec 31)
165- top tracks homepage (PR #684, Dec 31)
166- batch review system (PR #672, Dec 30)
167- CSS design tokens (PRs #662-664, Dec 29-30)
168- self-hosted redis migration (PRs #674-675, Dec 30)
169- supporter-gated content (PR #637, Dec 22-23)
170- supporter badges (PR #627, Dec 21-22)
171- end-of-year sprint: moderation + atprotofans (PRs #617-629, Dec 19-21)
172- offline mode foundation (PRs #610-611, Dec 17)
173- UX polish and login improvements (PRs #604-615, Dec 16-18)
174- visual customization with custom backgrounds (PRs #595-596, Dec 16)
175- performance & moderation polish (PRs #586-593, Dec 14-15)
176- mobile UI polish & background task expansion (PRs #558-572, Dec 10-12)
177- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13)
178- pagination & album management (PRs #550-554, Dec 9-10)
179- public cost dashboard (PRs #548-549, Dec 9)
180- docket background tasks & concurrent exports (PRs #534-546, Dec 9)
181- artist support links & inline playlist editing (PRs #520-532, Dec 8)
182- playlist fast-follow fixes (PRs #507-519, Dec 7-8)
183- playlists, ATProto sync, and library hub (PR #499, Dec 6-7)
184- sensitive image moderation (PRs #471-488, Dec 5-6)
185- teal.fm scrobbling (PR #467, Dec 4)
186- unified search with Cmd+K (PR #447, Dec 3)
187- light/dark theme system (PR #441, Dec 2-3)
188- tag filtering and bufo easter egg (PRs #431-438, Dec 2)
189
190### November 2025
191
192See `.status_history/2025-11.md` for detailed history including:
193- developer tokens (PR #367)
194- copyright moderation system (PRs #382-395)
195- export & upload reliability (PRs #337-344)
196- transcoder API deployment (PR #156)
197
198## priorities
199
200### current focus
201
202stabilization and polish after multi-account release. monitoring production for issues.
203
204**end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) shipped:**
205- moderation consolidation: sensitive images moved to moderation service (#644)
206- moderation batch review UI with Claude vision integration (#672, #687-690)
207- atprotofans: supporter badges (#627) and content gating (#637)
208
209### known issues
210- playback auto-start on refresh (#225)
211- iOS PWA audio may hang on first play after backgrounding
212
213### backlog
214- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred
215- share to bluesky (#334)
216- lyrics and annotations (#373)
217- configurable rules engine for moderation
218- time-release gating (#642)
219
220## technical state
221
222### architecture
223
224**backend**
225- language: Python 3.11+
226- framework: FastAPI with uvicorn
227- database: Neon PostgreSQL (serverless)
228- storage: Cloudflare R2 (S3-compatible)
229- background tasks: docket (Redis-backed)
230- hosting: Fly.io (2x shared-cpu VMs)
231- observability: Pydantic Logfire
232- auth: ATProto OAuth 2.1
233
234**frontend**
235- framework: SvelteKit (v2.43.2)
236- runtime: Bun
237- hosting: Cloudflare Pages
238- styling: vanilla CSS with lowercase aesthetic
239- state management: Svelte 5 runes
240
241**deployment**
242- ci/cd: GitHub Actions
243- backend: automatic on main branch merge (fly.io)
244- frontend: automatic on every push to main (cloudflare pages)
245- migrations: automated via fly.io release_command
246
247**what's working**
248
249**core functionality**
250- ✅ ATProto OAuth 2.1 authentication
251- ✅ multi-account support (link multiple ATProto identities)
252- ✅ secure session management via HttpOnly cookies
253- ✅ developer tokens with independent OAuth grants
254- ✅ platform stats and Media Session API
255- ✅ timed comments with clickable timestamps
256- ✅ artist profiles synced with Bluesky
257- ✅ track upload with streaming
258- ✅ audio streaming via 307 redirects to R2 CDN
259- ✅ play count tracking, likes, queue management
260- ✅ unified search with Cmd/Ctrl+K
261- ✅ teal.fm scrobbling
262- ✅ copyright moderation with ATProto labeler
263- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble)
264- ✅ media export with concurrent downloads
265- ✅ supporter-gated content via atprotofans
266
267**albums**
268- ✅ album CRUD with cover art
269- ✅ ATProto list records (auto-synced on login)
270
271**playlists**
272- ✅ full CRUD with drag-and-drop reordering
273- ✅ ATProto list records (synced on create/modify)
274- ✅ "add to playlist" menu, global search results
275
276**deployment URLs**
277- production frontend: https://plyr.fm
278- production backend: https://api.plyr.fm
279- staging: https://stg.plyr.fm / https://api-stg.plyr.fm
280
281### technical decisions
282
283**why Python/FastAPI instead of Rust?**
284- rapid prototyping velocity during MVP phase
285- trade-off: accepting higher latency for faster development
286
287**why Cloudflare R2 instead of S3?**
288- zero egress fees (critical for audio streaming)
289- S3-compatible API, integrated CDN
290
291**why async everywhere?**
292- I/O-bound workload: most time spent waiting on network/disk
293- PRs #149-151 eliminated all blocking operations
294
295## cost structure
296
297current monthly costs: ~$20/month (plyr.fm specific)
298
299see live dashboard: [plyr.fm/costs](https://plyr.fm/costs)
300
301- fly.io (backend + redis + moderation): ~$14/month
302- neon postgres: $5/month
303- cloudflare (R2 + pages + domain): ~$1/month
304- audd audio fingerprinting: $5-10/month (usage-based)
305- logfire: $0 (free tier)
306
307## admin tooling
308
309### content moderation
310script: `scripts/delete_track.py`
311
312usage:
313```bash
314uv run scripts/delete_track.py <track_id> --dry-run
315uv run scripts/delete_track.py <track_id>
316uv run scripts/delete_track.py --url https://plyr.fm/track/34
317```
318
319## for new contributors
320
321### getting started
3221. clone: `gh repo clone zzstoatzz/plyr.fm`
3232. install dependencies: `uv sync && cd frontend && bun install`
3243. run backend: `uv run uvicorn backend.main:app --reload`
3254. run frontend: `cd frontend && bun run dev`
3265. visit http://localhost:5173
327
328### development workflow
3291. create issue on github
3302. create PR from feature branch
3313. ensure pre-commit hooks pass
3324. merge to main → deploys to staging
3335. create github release → deploys to production
334
335### key principles
336- type hints everywhere
337- lowercase aesthetic
338- ATProto first
339- async everywhere (no blocking I/O)
340- mobile matters
341- cost conscious
342
343### project structure
344```
345plyr.fm/
346├── backend/ # FastAPI app & Python tooling
347│ ├── src/backend/ # application code
348│ ├── tests/ # pytest suite
349│ └── alembic/ # database migrations
350├── frontend/ # SvelteKit app
351│ ├── src/lib/ # components & state
352│ └── src/routes/ # pages
353├── moderation/ # Rust moderation service (ATProto labeler)
354├── transcoder/ # Rust audio transcoding service
355├── redis/ # self-hosted Redis config
356├── docs/ # documentation
357└── justfile # task runner
358```
359
360## documentation
361
362- [docs/README.md](docs/README.md) - documentation index
363- [runbooks](docs/runbooks/) - production incident procedures
364- [background tasks](docs/backend/background-tasks.md) - docket task system
365- [logfire querying](docs/tools/logfire.md) - observability queries
366- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content
367- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas
368
369---
370
371this is a living document. last updated 2026-01-07.