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#### supporter-gated content (PR #637, Dec 22-23)
51
52**atprotofans paywall integration** - artists can now mark tracks as "supporters only":
53- tracks with `support_gate` require atprotofans validation before playback
54- non-supporters see lock icon and "become a supporter" CTA linking to atprotofans
55- artists can always play their own gated tracks
56
57**backend architecture**:
58- audio endpoint validates supporter status via atprotofans API before serving gated content
59- HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues)
60- `R2Storage.move_audio()` moves files between public/private buckets when toggling gate
61- background task handles bucket migration asynchronously
62- ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`)
63
64**frontend**:
65- `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state
66- clicking locked track shows toast with CTA - does NOT interrupt current playback
67- portal shows support gate toggle in track edit UI
68
69---
70
71#### supporter badges (PR #627, Dec 21-22)
72
73**phase 1 of atprotofans integration**:
74- supporter badge displays on artist pages when logged-in viewer supports the artist
75- calls atprotofans `validateSupporter` API directly from frontend (public endpoint)
76- badge only shows when viewer is authenticated and not viewing their own profile
77
78---
79
80#### rate limit moderation endpoint (PR #629, Dec 21)
81
82**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.
83
84---
85
86#### end-of-year sprint planning (PR #626, Dec 20)
87
88**focus**: two foundational systems need solid experimental implementations by 2026.
89
90| track | focus | status |
91|-------|-------|--------|
92| moderation | consolidate architecture, add rules engine | in progress |
93| atprotofans | supporter validation, content gating | shipped (phase 1-3) |
94
95**research docs**:
96- [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md)
97- [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md)
98
99---
100
101#### beartype + moderation cleanup (PRs #617-619, Dec 19)
102
103**runtime type checking** (PR #619):
104- enabled beartype runtime type validation across the backend
105- catches type errors at runtime instead of silently passing bad data
106- test infrastructure improvements: session-scoped TestClient fixture (5x faster tests)
107
108**moderation cleanup** (PRs #617-618):
109- consolidated moderation code, addressing issues #541-543
110- `sync_copyright_resolutions` now runs automatically via docket Perpetual task
111- removed dead `init_db()` from lifespan (handled by alembic migrations)
112
113---
114
115#### UX polish (PRs #604-607, #613, #615, Dec 16-18)
116
117**login improvements** (PRs #604, #613):
118- login page now uses "internet handle" terminology for clarity
119- input normalization: strips `@` and `at://` prefixes automatically
120
121**artist page fixes** (PR #615):
122- track pagination on artist pages now works correctly
123- fixed mobile album card overflow
124
125**mobile + metadata** (PRs #605-607):
126- Open Graph tags added to tag detail pages for link previews
127- mobile modals now use full screen positioning
128- fixed `/tag/` routes in hasPageMetadata check
129
130---
131
132#### offline mode foundation (PRs #610-611, Dec 17)
133
134**experimental offline playback**:
135- storage layer using Cache API for audio bytes + IndexedDB for metadata
136- `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching
137- "auto-download liked" toggle in experimental settings section
138- Player checks for cached audio before streaming from R2
139
140---
141
142### Earlier December 2025
143
144See `.status_history/2025-12.md` for detailed history including:
145- visual customization with custom backgrounds (PRs #595-596, Dec 16)
146- performance & moderation polish (PRs #586-593, Dec 14-15)
147- mobile UI polish & background task expansion (PRs #558-572, Dec 10-12)
148- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13)
149- pagination & album management (PRs #550-554, Dec 9-10)
150- public cost dashboard (PRs #548-549, Dec 9)
151- docket background tasks & concurrent exports (PRs #534-546, Dec 9)
152- artist support links & inline playlist editing (PRs #520-532, Dec 8)
153- playlist fast-follow fixes (PRs #507-519, Dec 7-8)
154- playlists, ATProto sync, and library hub (PR #499, Dec 6-7)
155- sensitive image moderation (PRs #471-488, Dec 5-6)
156- teal.fm scrobbling (PR #467, Dec 4)
157- unified search with Cmd+K (PR #447, Dec 3)
158- light/dark theme system (PR #441, Dec 2-3)
159- tag filtering and bufo easter egg (PRs #431-438, Dec 2)
160
161### November 2025
162
163See `.status_history/2025-11.md` for detailed history including:
164- developer tokens (PR #367)
165- copyright moderation system (PRs #382-395)
166- export & upload reliability (PRs #337-344)
167- transcoder API deployment (PR #156)
168
169## immediate priorities
170
171### end-of-year sprint (Dec 20-31)
172
173see [sprint tracking issue #625](https://github.com/zzstoatzz/plyr.fm/issues/625) for details.
174
175| track | focus | status |
176|-------|-------|--------|
177| moderation | consolidate architecture, add rules engine | planning |
178| atprotofans | supporter validation, content gating | shipped |
179
180### known issues
181- playback auto-start on refresh (#225)
182- iOS PWA audio may hang on first play after backgrounding
183
184### backlog
185- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred
186- share to bluesky (#334)
187- lyrics and annotations (#373)
188
189## technical state
190
191### architecture
192
193**backend**
194- language: Python 3.11+
195- framework: FastAPI with uvicorn
196- database: Neon PostgreSQL (serverless)
197- storage: Cloudflare R2 (S3-compatible)
198- background tasks: docket (Redis-backed)
199- hosting: Fly.io (2x shared-cpu VMs)
200- observability: Pydantic Logfire
201- auth: ATProto OAuth 2.1
202
203**frontend**
204- framework: SvelteKit (v2.43.2)
205- runtime: Bun
206- hosting: Cloudflare Pages
207- styling: vanilla CSS with lowercase aesthetic
208- state management: Svelte 5 runes
209
210**deployment**
211- ci/cd: GitHub Actions
212- backend: automatic on main branch merge (fly.io)
213- frontend: automatic on every push to main (cloudflare pages)
214- migrations: automated via fly.io release_command
215
216**what's working**
217
218**core functionality**
219- ✅ ATProto OAuth 2.1 authentication
220- ✅ secure session management via HttpOnly cookies
221- ✅ developer tokens with independent OAuth grants
222- ✅ platform stats and Media Session API
223- ✅ timed comments with clickable timestamps
224- ✅ artist profiles synced with Bluesky
225- ✅ track upload with streaming
226- ✅ audio streaming via 307 redirects to R2 CDN
227- ✅ play count tracking, likes, queue management
228- ✅ unified search with Cmd/Ctrl+K
229- ✅ teal.fm scrobbling
230- ✅ copyright moderation with ATProto labeler
231- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble)
232- ✅ media export with concurrent downloads
233- ✅ supporter-gated content via atprotofans
234
235**albums**
236- ✅ album CRUD with cover art
237- ✅ ATProto list records (auto-synced on login)
238
239**playlists**
240- ✅ full CRUD with drag-and-drop reordering
241- ✅ ATProto list records (synced on create/modify)
242- ✅ "add to playlist" menu, global search results
243
244**deployment URLs**
245- production frontend: https://plyr.fm
246- production backend: https://api.plyr.fm
247- staging: https://stg.plyr.fm / https://api-stg.plyr.fm
248
249### technical decisions
250
251**why Python/FastAPI instead of Rust?**
252- rapid prototyping velocity during MVP phase
253- trade-off: accepting higher latency for faster development
254
255**why Cloudflare R2 instead of S3?**
256- zero egress fees (critical for audio streaming)
257- S3-compatible API, integrated CDN
258
259**why async everywhere?**
260- I/O-bound workload: most time spent waiting on network/disk
261- PRs #149-151 eliminated all blocking operations
262
263## cost structure
264
265current monthly costs: ~$18/month (plyr.fm specific)
266
267see live dashboard: [plyr.fm/costs](https://plyr.fm/costs)
268
269- fly.io (plyr apps only): ~$12/month
270- neon postgres: $5/month
271- cloudflare (R2 + pages + domain): ~$1.16/month
272- audd audio fingerprinting: $0-10/month (6000 free/month)
273- logfire: $0 (free tier)
274
275## admin tooling
276
277### content moderation
278script: `scripts/delete_track.py`
279
280usage:
281```bash
282uv run scripts/delete_track.py <track_id> --dry-run
283uv run scripts/delete_track.py <track_id>
284uv run scripts/delete_track.py --url https://plyr.fm/track/34
285```
286
287## for new contributors
288
289### getting started
2901. clone: `gh repo clone zzstoatzz/plyr.fm`
2912. install dependencies: `uv sync && cd frontend && bun install`
2923. run backend: `uv run uvicorn backend.main:app --reload`
2934. run frontend: `cd frontend && bun run dev`
2945. visit http://localhost:5173
295
296### development workflow
2971. create issue on github
2982. create PR from feature branch
2993. ensure pre-commit hooks pass
3004. merge to main → deploys to staging
3015. create github release → deploys to production
302
303### key principles
304- type hints everywhere
305- lowercase aesthetic
306- ATProto first
307- async everywhere (no blocking I/O)
308- mobile matters
309- cost conscious
310
311### project structure
312```
313plyr.fm/
314├── backend/ # FastAPI app & Python tooling
315│ ├── src/backend/ # application code
316│ ├── tests/ # pytest suite
317│ └── alembic/ # database migrations
318├── frontend/ # SvelteKit app
319│ ├── src/lib/ # components & state
320│ └── src/routes/ # pages
321├── moderation/ # Rust moderation service (ATProto labeler)
322├── transcoder/ # Rust audio transcoding service
323├── docs/ # documentation
324└── justfile # task runner
325```
326
327## documentation
328
329- [docs/README.md](docs/README.md) - documentation index
330- [runbooks](docs/runbooks/) - production incident procedures
331- [background tasks](docs/backend/background-tasks.md) - docket task system
332- [logfire querying](docs/tools/logfire.md) - observability queries
333- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content
334- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas
335
336---
337
338this is a living document. last updated 2025-12-23.