music on atproto
plyr.fm
1# configuration
2
3plyr.fm uses nested pydantic settings for configuration management, following a pattern similar to prefect.
4
5## settings structure
6
7settings are organized into logical sections:
8
9```python
10from backend.config import settings
11
12# application settings
13settings.app.name # "plyr"
14settings.app.port # 8001 (from PORT)
15settings.app.debug # false
16settings.app.broadcast_channel_prefix # "plyr"
17settings.app.canonical_host # "plyr.fm"
18settings.app.canonical_url # computed: https://plyr.fm
19
20# frontend settings
21settings.frontend.url # from FRONTEND_URL
22settings.frontend.cors_origin_regex # from FRONTEND_CORS_ORIGIN_REGEX (optional)
23settings.frontend.resolved_cors_origin_regex # computed: defaults to relay-4i6.pages.dev pattern
24
25# database settings
26settings.database.url # from DATABASE_URL
27
28# storage settings (cloudflare r2)
29settings.storage.backend # from STORAGE_BACKEND
30settings.storage.r2_bucket # from R2_BUCKET (public audio files)
31settings.storage.r2_private_bucket # from R2_PRIVATE_BUCKET (gated audio files)
32settings.storage.r2_image_bucket # from R2_IMAGE_BUCKET (image files)
33settings.storage.r2_endpoint_url # from R2_ENDPOINT_URL
34settings.storage.r2_public_bucket_url # from R2_PUBLIC_BUCKET_URL (audio files)
35settings.storage.r2_public_image_bucket_url # from R2_PUBLIC_IMAGE_BUCKET_URL (image files)
36settings.storage.aws_access_key_id # from AWS_ACCESS_KEY_ID
37settings.storage.aws_secret_access_key # from AWS_SECRET_ACCESS_KEY
38
39# atproto settings
40settings.atproto.pds_url # from ATPROTO_PDS_URL
41settings.atproto.client_id # from ATPROTO_CLIENT_ID
42settings.atproto.client_secret # from ATPROTO_CLIENT_SECRET
43settings.atproto.redirect_uri # from ATPROTO_REDIRECT_URI
44settings.atproto.app_namespace # from ATPROTO_APP_NAMESPACE
45settings.atproto.old_app_namespace # from ATPROTO_OLD_APP_NAMESPACE (optional)
46settings.atproto.oauth_encryption_key # from OAUTH_ENCRYPTION_KEY
47settings.atproto.track_collection # computed: "{namespace}.track"
48settings.atproto.old_track_collection # computed: "{old_namespace}.track" (if set)
49settings.atproto.resolved_scope # computed: "atproto repo:{collections}"
50
51# observability settings (pydantic logfire)
52settings.observability.enabled # from LOGFIRE_ENABLED
53settings.observability.write_token # from LOGFIRE_WRITE_TOKEN
54settings.observability.environment # from LOGFIRE_ENVIRONMENT
55settings.observability.suppressed_loggers # from LOGFIRE_SUPPRESSED_LOGGERS (default: {"docket"})
56
57# notification settings
58settings.notify.enabled # from NOTIFY_ENABLED
59settings.notify.recipient_handle # from NOTIFY_RECIPIENT_HANDLE
60settings.notify.bot.handle # from NOTIFY_BOT_HANDLE
61settings.notify.bot.password # from NOTIFY_BOT_PASSWORD
62
63# background task settings (docket/redis)
64settings.docket.name # "plyr" (queue namespace)
65settings.docket.url # from DOCKET_URL (empty = disabled)
66settings.docket.worker_concurrency # 10 (concurrent tasks)
67```
68
69## environment variables
70
71all settings can be configured via environment variables. the variable names match the flat structure used in `.env`:
72
73### required
74
75```bash
76# database
77DATABASE_URL=postgresql+psycopg://user:pass@host/db
78
79# oauth (uses client metadata discovery - no registration required)
80ATPROTO_CLIENT_ID=https://your-domain.com/oauth-client-metadata.json
81ATPROTO_CLIENT_SECRET=<optional-client-secret>
82ATPROTO_REDIRECT_URI=https://your-domain.com/auth/callback
83OAUTH_ENCRYPTION_KEY=<base64-encoded-32-byte-key>
84
85# storage
86STORAGE_BACKEND=r2 # or "filesystem"
87R2_BUCKET=your-audio-bucket
88R2_PRIVATE_BUCKET=your-private-audio-bucket # for supporter-gated content
89R2_IMAGE_BUCKET=your-image-bucket
90R2_ENDPOINT_URL=https://xxx.r2.cloudflarestorage.com
91R2_PUBLIC_BUCKET_URL=https://pub-xxx.r2.dev # for audio files
92R2_PUBLIC_IMAGE_BUCKET_URL=https://pub-xxx.r2.dev # for image files
93AWS_ACCESS_KEY_ID=your-r2-access-key
94AWS_SECRET_ACCESS_KEY=your-r2-secret
95```
96
97### optional
98
99```bash
100# app
101PORT=8001
102FRONTEND_URL=http://localhost:5173
103
104# observability
105LOGFIRE_ENABLED=true
106LOGFIRE_WRITE_TOKEN=pylf_xxx
107LOGFIRE_SUPPRESSED_LOGGERS=docket # comma-separated, suppress noisy loggers
108
109# notifications (bluesky DMs)
110NOTIFY_ENABLED=true
111NOTIFY_RECIPIENT_HANDLE=your.handle
112NOTIFY_BOT_HANDLE=bot.handle
113NOTIFY_BOT_PASSWORD=app-password
114
115# background tasks (docket/redis)
116DOCKET_URL=redis://localhost:6379 # or rediss:// for TLS
117DOCKET_NAME=plyr # queue namespace (default: plyr)
118DOCKET_WORKER_CONCURRENCY=10 # concurrent task limit (default: 10)
119```
120
121## computed fields
122
123some settings are computed from other values:
124
125### `settings.app.canonical_url`
126
127automatically determines the protocol based on host:
128- `localhost` or `127.0.0.1` → `http://`
129- anything else → `https://`
130
131can be overridden with `canonical_url_override` if needed.
132
133### `settings.frontend.resolved_cors_origin_regex`
134
135constructs the CORS origin regex pattern:
136```python
137# default: allows localhost + relay-4i6.pages.dev (including preview deployments)
138r"^(https://([a-z0-9]+\.)?relay-4i6\.pages\.dev|http://localhost:5173)$"
139```
140
141can be overridden with `FRONTEND_CORS_ORIGIN_REGEX` if needed.
142
143### `settings.atproto.track_collection`
144
145constructs the atproto collection name from the namespace:
146```python
147f"{settings.atproto.app_namespace}.track"
148# default: "fm.plyr.track"
149```
150
151### `settings.atproto.resolved_scope`
152
153constructs the oauth scope from the collection(s):
154```python
155# base scopes: our track collection + our like collection
156scopes = [
157 f"repo:{settings.atproto.track_collection}",
158 f"repo:{settings.atproto.app_namespace}.like",
159]
160
161# if we have an old namespace, add old track collection too
162if settings.atproto.old_app_namespace:
163 scopes.append(f"repo:{settings.atproto.old_track_collection}")
164
165return f"atproto {' '.join(scopes)}"
166# default: "atproto repo:fm.plyr.track repo:fm.plyr.like"
167```
168
169can be overridden with `ATPROTO_SCOPE_OVERRIDE` if needed.
170
171## atproto namespace
172
173plyr.fm uses `fm.plyr` as the ATProto namespace:
174
175```bash
176ATPROTO_APP_NAMESPACE=fm.plyr # default
177```
178
179this defines the collections:
180- `track_collection` → `"fm.plyr.track"`
181- `like_collection` → `"fm.plyr.like"` (implicit)
182- `resolved_scope` → `"atproto repo:fm.plyr.track repo:fm.plyr.like"`
183
184### environment-specific namespaces
185
186each environment uses a separate namespace to prevent test data from polluting production collections:
187
188**development (local):**
189```bash
190ATPROTO_APP_NAMESPACE=fm.plyr.dev
191```
192- `track_collection` → `"fm.plyr.dev.track"`
193- `like_collection` → `"fm.plyr.dev.like"`
194- records written to dev-specific collections on user's PDS
195
196**staging:**
197```bash
198ATPROTO_APP_NAMESPACE=fm.plyr.stg
199```
200- `track_collection` → `"fm.plyr.stg.track"`
201- `like_collection` → `"fm.plyr.stg.like"`
202- records written to staging-specific collections on user's PDS
203
204**production:**
205```bash
206ATPROTO_APP_NAMESPACE=fm.plyr
207```
208- `track_collection` → `"fm.plyr.track"`
209- `like_collection` → `"fm.plyr.like"`
210- records written to production collections on user's PDS
211
212this separation ensures that:
213- test tracks/likes created in dev/staging don't pollute production collections
214- OAuth scopes are environment-specific
215- database and ATProto records stay aligned within each environment
216
217see `docs/deployment/environments.md` for more details on environment configuration.
218
219### namespace migration
220
221optionally supports migration from an old namespace:
222
223```bash
224ATPROTO_OLD_APP_NAMESPACE=app.relay # optional, for migration
225```
226
227when set, OAuth scopes will include both old and new namespaces:
228- `old_track_collection` → `"app.relay.track"`
229- `resolved_scope` → `"atproto repo:fm.plyr.track repo:fm.plyr.like repo:app.relay.track"`
230
231## usage in code
232
233```python
234from backend.config import settings
235
236# access nested settings
237database_url = settings.database.url
238r2_bucket = settings.storage.r2_bucket
239track_collection = settings.atproto.track_collection
240
241# computed properties work seamlessly
242canonical_url = settings.app.canonical_url
243oauth_scope = settings.atproto.resolved_scope
244```
245
246## testing
247
248tests automatically override settings with local defaults via `tests/__init__.py`:
249
250```python
251os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://localhost/relay")
252os.environ.setdefault("LOGFIRE_ENABLED", "false")
253os.environ.setdefault("NOTIFY_ENABLED", "false")
254```
255
256individual tests can override settings using pytest fixtures:
257
258```python
259from backend.config import Settings
260
261def test_something(monkeypatch):
262 monkeypatch.setenv("PORT", "9100")
263 monkeypatch.setenv("ATPROTO_APP_NAMESPACE", "com.example.test")
264
265 settings = Settings() # reload with new env vars
266 assert settings.app.port == 9100
267 assert settings.atproto.track_collection == "com.example.test.track"
268```
269
270## migration from flat settings
271
272the refactor maintains backward compatibility with all existing environment variables:
273
274| old (flat) | new (nested) | env var |
275|-------------------------|---------------------------------|-------------------|
276| `settings.port` | `settings.app.port` | `PORT` |
277| `settings.database_url` | `settings.database.url` | `DATABASE_URL` |
278| `settings.r2_bucket` | `settings.storage.r2_bucket` | `R2_BUCKET` |
279| `settings.atproto_scope`| `settings.atproto.resolved_scope`| (computed) |
280
281all code has been updated to use the nested structure.
282
283## design
284
285the settings design follows the prefect pattern:
286- each section extends `BaseSettings` subclass
287- sections are composed in the root `Settings` class
288- environment variables map directly to field names
289- computed fields derive values from other settings
290- type hints ensure correct types at runtime
291
292see `src/backend/config.py` for implementation details.