# ADR-0002: Owner defaults live in the DB row; .env keeps deployment config and secrets **Status**: accepted (2026-09-27) ## Context All configuration lived in `.env`: bind address, media paths, Ollama/llama-swap/OpenRouter URLs, model names, `LLM_BACKEND`, the embedding contract, TTS voices. Two friction points: values the owner actually *tunes* (which model, which backend, which persona, how long a reel is) needed an edit + redeploy to change, and clients had no server-side fallback — "use the default" was expressed by omitting a field, and the default itself was invisible and untouchable at runtime. The `user_ai_prefs` single row (`id = 1`) already existed as a passive mirror of the last client reel request for the nightly pre-gen. #126/#127 gave it an explicit `PUT /prefs` surface and five owner-default columns (`default_model`, `default_backend`, `default_persona_id`, `reel_span`, `reel_max_segments`) — the first batch of non-private configuration moved out of `.env` and into the database. ## Decision Configuration splits into two homes by one rule — *does the server need the value before it can start serving?* - **`.env` (deployment config)**: everything read at boot to bind, connect, or open — `BIND_URL`, `DATABASE_URL`, media paths, `OLLAMA_*_URL` / `LLAMA_SWAP_URL`, `LLM_BACKEND`, the embedding contract — and **all secrets** (`OPENROUTER_API_KEY`, `SECRET_KEY`, `SMS_API_TOKEN`). Secrets never go in the database: the SQLite file gets backed up, copied, and attached to bug reports. - **`user_ai_prefs` (owner defaults)**: per-request defaults resolved at request time — the model/backend/persona a client gets when it omits the field, reel span and segment cap, display zone, TTS voice. Hot-tunable via `PUT /prefs`, effective on the next request, no redeploy. This row is the designated home for future non-private, per-request configuration that outgrows `.env`. The table is a **single row per deployment, not a per-user profile** — `user_ai_prefs` is a historical name; the canonical term is *owner defaults* (single-user tool: owner = admin). Resolution order at every consumer: **explicit request param → stored owner default → hard default** (the env/code fallback). An explicit request always wins, so any client can override the owner's default per request. The env value stays the floor under the DB: a NULL `default_model` means "whatever the Ollama/llama-swap primary is", so env changes keep working. **Privacy posture is safe-by-default.** `default_backend` is the one owner default with network-egress consequences: `hybrid` routes insight generation and chat to OpenRouter. With no row or a NULL column, the hard default is `local` — data stays on the LAN. Leaving the network requires an explicit owner action (storing `hybrid`), and any client can still pin itself by sending `backend` explicitly. Owner defaults therefore split into two sensitivity classes: **harmless tuning** (span, segments, display zone, voice, model, persona) and **privacy/security** (`default_backend`), which any admin UI must surface with a warning, not a quiet dropdown. ## Considered options - **Everything in `.env`** (status quo): safe, but tuning a model default means edit + redeploy, and clients have no server-side default to fall back to — a picker's "server default" is just the env value, invisible and unchangeable at runtime. Rejected for per-request defaults. - **Everything in the DB**: boot-time values (bind address, DB path, the URLs used to build the HTTP clients) become chicken-and-egg, and secrets sit next to tuning values in a file that gets copied around. Rejected. - **A separate `system_config` table**: cleaner name, but a second single-row table would be a synonym with extra migrations; the row and the `PUT /prefs` surface already exist. Rejected — the row keeps its name, its *term* becomes owner defaults. ## Non-obvious rules worth remembering - **Owner defaults are global to the deployment.** Every client that omits a field gets the stored value, on every device. Storing `default_backend=hybrid` changes the mobile app's behavior even for users who never touch the setting; a client that must stay local should send `backend` explicitly (tracked for FileViewer-React #33). - **A stored value that is no longer consumable fails at write time, not request time** — `PUT /prefs` validates `default_backend` against the canonical `BackendKind::parse` and `reel_span` against the span enum before writing, so a bad default cannot 500 every request later. - **The passive reel mirror (`capture_prefs`) may only write the fields it owns** (voice, library, display zone, effective span/segments); it carries the AI owner-defaults through unchanged, because the upsert overwrites every column. - **NULL means "use the hard default", not "unset the env"** — the DB layer sits *above* env, never instead of it.