plyr.fm documentation#
this directory contains all documentation for the plyr.fm project.
documentation index#
authentication & security#
- authentication.md - secure cookie-based authentication, HttpOnly cookies, XSS protection, environment architecture, migration from localStorage
frontend#
- state-management.md - global state management with Svelte 5 runes (toast notifications, tracks cache, upload manager, queue management, liked tracks, preferences, localStorage persistence)
- toast-notifications.md - user feedback system for async operations with smooth transitions and auto-dismiss
- queue.md - music queue management with server sync
- keyboard-shortcuts.md - global keyboard shortcuts with context-aware filtering (Q for queue toggle, patterns for adding new shortcuts)
backend#
- configuration.md - backend configuration and environment setup
- liked-tracks.md - ATProto-backed track likes with error handling and consistency guarantees
- streaming-uploads.md - SSE-based progress tracking for file uploads with fire-and-forget pattern
- transcoder.md - rust-based HTTP service for audio format conversion (ffmpeg integration, authentication, fly.io deployment)
deployment#
- environments.md - staging vs production environments, automated deployment via GitHub Actions, CORS, secrets management
- database-migrations.md - automated migration workflow via fly.io release commands, alembic usage, safety procedures
tools#
- logfire.md - SQL query patterns for Logfire DataFusion database, finding exceptions, analyzing performance bottlenecks
- neon.md - Neon Postgres database management and best practices
- pdsx.md - ATProto PDS explorer and debugging tools
local development#
- setup.md - complete local development setup guide
ATProto integration#
plyr.fm uses a hybrid storage model:
- audio files stored in cloudflare R2 (scalable, CDN-backed)
- metadata stored as ATProto records on user's PDS (decentralized, user-owned)
- local database indexes for fast queries
key namespaces:
fm.plyr.track- track metadata (title, artist, album, features, image, audio file reference)fm.plyr.like- user likes on tracks (subject references track URI)
quick start#
current state#
plyr.fm is fully functional with:
- ✅ OAuth 2.1 authentication (ATProto)
- ✅ secure cookie-based sessions (HttpOnly, XSS protection)
- ✅ R2 storage for audio files (cloudflare CDN)
- ✅ track upload with streaming (prevents OOM)
- ✅ ATProto record creation (fm.plyr.track namespace)
- ✅ music player with queue management
- ✅ liked tracks (fm.plyr.like namespace)
- ✅ artist pages and track discovery
- ✅ share buttons across track, album, and artist detail pages for quick copy-to-clipboard links
- ✅ modular audio player with dedicated subcomponents for metadata, transport, progress, and volume controls
- ✅ image uploads for track artwork
- ✅ audio transcoding service (rust + ffmpeg)
- ✅ server-sent events for upload progress
- ✅ toast notifications
- ✅ user preferences (accent color, auto-play)
- ✅ keyboard shortcuts (Q for queue toggle)
local development#
see local-development/setup.md for complete setup instructions.
quick start:
# backend
uv run uvicorn backend.main:app --reload --host 0.0.0.0 --port 8001
# frontend
cd frontend && bun run dev
# transcoder (optional)
cd transcoder && just run
deployment#
see deployment/environments.md for details on:
- staging vs production environments
- automated deployment via GitHub Actions
- environment variables and secrets
see deployment/database-migrations.md for:
- migration workflow and safety procedures
- alembic usage and testing
architecture decisions#
why R2 instead of PDS blobs?#
PDS blobs are designed for smaller files like images. audio files are:
- larger (5-50MB per track)
- require streaming
- benefit from CDN distribution
R2 provides:
- scalable storage
- free egress to cloudflare CDN
- simple HTTP URLs
- cost-effective (~$0.015/GB/month)
why fm.plyr namespace?#
plyr.fm uses fm.plyr.* as the ATProto namespace:
fm.plyr.trackfor track metadatafm.plyr.likefor user likes
this is a domain-specific lexicon that allows:
- clear ownership and governance
- faster iteration without formal approval
- alignment with the plyr.fm brand
why hybrid storage?#
storing metadata on ATProto provides:
- user data sovereignty (users own their catalog)
- decentralization (no single point of failure)
- portability (users can move to another client)
storing audio on R2 provides:
- performance (fast streaming via CDN)
- scalability (handles growth)
- cost efficiency (cheaper than PDS blobs)
why separate transcoder service?#
the transcoder runs as a separate rust service because:
- ffmpeg operations are CPU-intensive and can block event loop
- rust provides better performance for media processing
- isolation prevents transcoding from affecting API latency
- can scale independently from main backend
testing#
plyr.fm uses pytest for backend testing:
# run all tests
just test
# run specific test file
just test tests/api/test_track_likes.py
# run with verbose output
just test -v
test categories:
- API endpoints (
tests/api/) - storage backends (
tests/storage/) - ATProto integration (
tests/test_atproto.py) - audio format validation (
tests/test_audio_formats.py)
see tests/CLAUDE.md for testing guidelines.
troubleshooting#
R2 upload fails#
error: failed to upload to R2
check:
- R2 credentials in
.env - bucket exists and is accessible
- account ID is correct
ATProto record creation fails#
error: failed to create atproto record
check:
- OAuth session is valid (not expired)
- user has write permissions
- PDS is accessible
- record format is valid
audio won't play#
404: audio file not found
check:
STORAGE_BACKENDmatches actual storage- R2 bucket has public read access
- file_id matches database record
monitoring#
key metrics to track#
-
upload success rate
- total uploads attempted
- successful R2 uploads
- successful record creations
-
storage costs
- total R2 storage (GB)
- monthly operations count
- estimated cost
-
playback metrics
- tracks played
- average stream duration
- errors/failures
logging#
add structured logging for debugging:
import structlog
logger = structlog.get_logger()
logger.info(
"track_uploaded",
track_id=track.id,
r2_url=r2_url,
atproto_uri=atproto_uri,
)
security considerations#
audio file access#
current: R2 URLs are public (anyone with URL can access)
acceptable for MVP because:
- music is meant to be shared
- no sensitive content
- URL guessing is impractical (content-based hashes)
future enhancement: signed URLs with expiration
record ownership#
enforced by ATProto: only user with valid OAuth session can create records in their repo
enforced by backend: tracks are associated with artist_did and only owner can delete
rate limiting#
recommended: limit uploads to prevent abuse
- 10 uploads per hour per user
- 100MB total per hour per user
cost estimates#
current monthly costs (~$15-20/month):
- fly.io backend: $5-10/month (shared-cpu-1x, 256MB RAM)
- fly.io transcoder: $5-10/month (shared-cpu-1x, 256MB RAM)
- neon postgres: free tier (0.5GB storage, 3GB data transfer)
- cloudflare R2: ~$0.16/month (6 buckets: audio-dev, audio-stg, audio-prod, images-dev, images-stg, images-prod)
- cloudflare pages: free (frontend hosting)
R2 storage scaling (audio + images):
- 1,000 tracks: ~$0.16/month
- 10,000 tracks: ~$1.58/month
- 100,000 tracks: ~$15.81/month
references#
ATProto documentation#
cloudflare documentation#
plyr.fm project files#
- project instructions:
CLAUDE.md - main readme:
README.md - justfile:
justfile(task runner) - backend:
src/backend/ - frontend:
frontend/ - transcoder:
transcoder/
contributing#
when working on plyr.fm:
- test empirically first - run code and prove it works
- reference existing docs - check docs directory before researching
- keep it simple - MVP over perfection
- use lowercase - respect plyr.fm's aesthetic
- no sprawl - avoid creating multiple versions of files
- document decisions - update docs as you work
questions?#
if anything is unclear:
- check the relevant phase document
- review example projects in sandbox
- consult ATProto official docs
- look at your atproto fork implementation