diff --git a/docs/plans/2026-02-18-config-file-design.md b/docs/plans/2026-02-18-config-file-design.md new file mode 100644 index 0000000..d366e91 --- /dev/null +++ b/docs/plans/2026-02-18-config-file-design.md @@ -0,0 +1,84 @@ +# TOML Configuration File Design + +**Date:** 2026-02-18 +**Status:** Approved + +## Goal + +Add a TOML configuration file to Porchlight so that server settings and OIDC +client registrations can be defined in a file. Environment variables retain +highest priority and override any file-based values. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| File format | TOML | Human-friendly, supports comments, stdlib `tomllib` on 3.11+ | +| Integration | pydantic-settings `TomlConfigSettingsSource` | Already available in pydantic-settings 2.12, handles precedence natively | +| Default path | `porchlight.toml` in CWD | Simple convention, overridable via `OIDC_OP_CONFIG_FILE` | +| Missing file | Silently skip | Env vars and defaults still work — file is optional | +| Scope | Server settings + client registrations in one file | Single source of truth | + +## Precedence + +``` +env vars > TOML file > defaults +``` + +pydantic-settings handles this via `settings_customise_sources()`. + +## Example porchlight.toml + +```toml +issuer = "https://auth.example.com" +debug = false +session_secret = "a-long-random-string" +sqlite_path = "data/porchlight.db" +signing_key_path = "data/keys" +invite_ttl = 86400 + +[clients.my-webapp] +client_secret = "super-secret-value" +redirect_uris = ["https://app.example.com/callback"] +response_types = ["code"] +scope = ["openid", "profile", "email"] +token_endpoint_auth_method = "client_secret_basic" + +[clients.another-app] +client_secret = "another-secret" +redirect_uris = ["https://other.example.com/oidc/callback"] +response_types = ["code"] +scope = ["openid", "profile"] +token_endpoint_auth_method = "client_secret_basic" +``` + +## Code Changes + +### config.py + +- Add `ClientConfig(BaseModel)` with fields: `client_secret`, `redirect_uris`, + `response_types` (default `["code"]`), `scope` (default `["openid"]`), + `token_endpoint_auth_method` (default `"client_secret_basic"`). +- Add `config_file: str = "porchlight.toml"` field to `Settings`. +- Add `clients: dict[str, ClientConfig] = {}` field to `Settings`. +- Override `settings_customise_sources()` to insert `TomlConfigSettingsSource` + between env and defaults. Use `config_file` field as the TOML path. + +### app.py + +- After creating the OIDC server, loop over `settings.clients` and register + each client in `oidc_server.context.cdb`. +- Keep the internal `manage-app` client registration as-is (always registered + regardless of config file). + +### cli.py + +- No changes. `Settings()` picks up the TOML file automatically. + +## What Stays the Same + +- All existing `OIDC_OP_*` env vars work identically. +- `Settings()` call sites unchanged. +- Docker/compose env var configuration unchanged. +- No new dependencies (pydantic-settings 2.12 already has TOML support, + Python 3.13 has `tomllib` in stdlib).