Configuration
Every setting is read once at startup via pydantic-settings with env_prefix="KX_", extra="forbid" and frozen=True. Invalid combination fails fast with a ValueError before the app binds.
Environment variables
| Variable | Type | Default | Validation |
|---|---|---|---|
KX_HOST | str | 127.0.0.1 | bind target; 0.0.0.0 only if intentionally exposing on LAN |
KX_PORT | int 1–65535 | 8000 | — |
KX_WORKERS | int >=1 | 1 | !=1 rejected — JSON store is single-process |
KX_RUNTIME_ROOT | Path | runtime | created 0o700; holds state.json, runtime.lock, uploads/, .kxinspect-runtime |
KX_SEED_ROOT | Path | app/seed | seed JSON directory |
KX_STATIC_ROOT | Path | app/static/photos | seed media dir |
KX_ENABLE_DEV_ROUTES | bool | false | when false /_dev/* not even bound |
KX_DEV_TOKEN | str | "" | required non-empty when dev routes enabled; checked hmac.compare_digest |
KX_CORS_ORIGINS | tuple[str,…] (comma-split) | http://localhost:8080,http://127.0.0.1:8080 | * rejected |
KX_GRACE_PERIOD_DAYS | int 0–3650 | 30 | used when dev-raised charge omits it |
KX_DEMO_NOW | str? (ISO-8601 UTC Z) | None | when set, AnchoredDemoClock replaces SystemClock |
KX_SSE_HEARTBEAT_SECONDS | float >0 | 15.0 | SSE comment heartbeat interval |
KX_SSE_QUEUE_CAPACITY | int >=1 | 64 | per-client broker queue |
Parsing helpers: cors_origins comma-split and trimmed, wildcard reject; demo_now validated via parse_instant (rejects offsets/naive); model_validator enforces workers==1 + token non-empty when needed.
Derived paths
settings.state_path # runtime/state.json
settings.uploads_root # runtime/uploads
settings.staging_root # runtime/uploads/.staging
settings.lock_path # runtime/runtime.lock
settings.marker_path # runtime/.kxinspect-runtime (RUNTIME_MARKER_UUID guard)
Constants (app/core/config.py)
REFERENCE_NOW = "2026-08-01T12:00:00Z"
CONTEST_REASON_MIN = 10 # graphemes
CONTEST_REASON_MAX = 2000
MAX_ATTACHMENTS = 5
MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024
MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024
EVENT_RING_CAPACITY = 100
SCHEMA_VERSION = CONTRACT_VERSION = SEED_VERSION = API_MAJOR = "1"
Runtime files
runtime/
├── state.json # StoreSnapshot (atomically replaced)
├── runtime.lock # advisory flock, pid written
├── .kxinspect-runtime # RUNTIME_MARKER_UUID
├── uploads/
│ ├── att_<32hex> # finalized uploads (private)
│ └── .staging/<reqId>/ # transient per-request staging
└── .marker
Directory runtime/ is 700; lock open mode 0o600.
CORS & headers
Allowed headers: Content-Type, Idempotency-Key, Last-Event-ID, X-Dev-Token. Allowed methods: GET, POST, OPTIONS. Wildcard origin * rejected at startup; explicitly list every web origin.
Dev routes guard
When KX_ENABLE_DEV_ROUTES=true the router includes app/api/v1/endpoints/dev.py; every handler performs if not hmac.compare_digest(token, settings.dev_token): raise 401. Loopback does not bypass.
Example invocations
# production-like demo anchor (deterministic deadlines)
KX_DEMO_NOW=2026-08-01T12:00:00Z KX_CORS_ORIGINS=http://localhost:3000 \
uv run uvicorn app.main:create_app --factory --host 127.0.0.1 --port 8000
# with dev reset enabled
KX_ENABLE_DEV_ROUTES=true KX_DEV_TOKEN=local-demo \
uv run uvicorn app.main:create_app --factory
# custom runtime location for parallel tests
KX_RUNTIME_ROOT=/tmp/kx-test-$$ \
uv run pytest -q
No config is read outside Settings; tests inject Settings(runtime_root=tmp) + ManualClock to keep CI deterministic.