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