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