ADR-0002: owner defaults live in the DB row; .env keeps deployment config and secrets
Records the config boundary the #127 owner-default columns establish: .env = boot-time deployment config + secrets (never DB), user_ai_prefs single row = per-request owner defaults (hot-tunable, no secrets), resolution request param -> owner default -> hard default, and the safe-by-default privacy posture for default_backend. Adds the Configuration glossary section to CONTEXT.md (owner default, deployment config, hard default).
This commit is contained in:
1 parent
a699c940b8
commit
e84ac82f86
2 files changed
+100
No files matched your search
+22
@@ -62,3 +62,25 @@ Insights and pre-computed artifacts grouped by calendar day (daily message summa
|
|||||||
reel pre-generation). Because it is keyed on the day, re-dating a photo across a day
|
reel pre-generation). Because it is keyed on the day, re-dating a photo across a day
|
||||||
boundary invalidates the derived data for that day, which regenerates on its normal schedule.
|
boundary invalidates the derived data for that day, which regenerates on its normal schedule.
|
||||||
_Avoid_: cache, nightly output
|
_Avoid_: cache, nightly output
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
**Owner default**:
|
||||||
|
A per-request default the owner sets for the whole deployment — the model, backend,
|
||||||
|
persona, reel span, or segment cap a client gets when it omits the field. Stored in the
|
||||||
|
single `user_ai_prefs` row, hot-tunable without a redeploy, and global: every client that
|
||||||
|
omits the field gets it, on every device. Never holds secrets.
|
||||||
|
_Avoid_: user prefs, user settings, preferences (ambiguous with device-local client state)
|
||||||
|
|
||||||
|
**Deployment config**:
|
||||||
|
Values the server needs before it can start serving — bind address, database path, media
|
||||||
|
roots, the URLs used to build clients — plus every secret. Lives in `.env` only; changing
|
||||||
|
it is a redeploy, by design.
|
||||||
|
_Avoid_: environment config, system config
|
||||||
|
|
||||||
|
**Hard default**:
|
||||||
|
The env/code fallback a request lands on when neither an explicit param nor a stored
|
||||||
|
owner default supplies the field (e.g. backend `local`, persona `default`, reel span
|
||||||
|
day). It is the floor under the owner-default layer: env changes keep working whenever a
|
||||||
|
default column is NULL.
|
||||||
|
_Avoid_: fallback value, env default
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 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.
|
||||||
Reference in new issue
Block a user