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.