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