ADR 0001: Configuration, Settings, Runtime State, And Taxonomy
- Status: accepted
- Date: 2026-08-20
- Decision owners: core architecture and the owning runtime modules
Context
AppSettings began as a convenient tenant key/value store. Over time, tenant
preferences, credentials, import cursors, job snapshots, and timestamps acquired
similar storage shapes even though they have different ownership, lifecycle,
security, and deployment semantics. Separately, several modules use the words
"category" or "tag" for concepts that are not interchangeable.
An unrestricted variable bag would preserve this ambiguity. A generic taxonomy schema created from names alone would move module invariants into core without proven consumers.
Decision
Every new value belongs to exactly one of these classes:
| Class | Owner and lifecycle | Writable at request time | Examples |
|---|---|---|---|
| Bootstrap configuration | deployment; required before the tenant database or container is available | no | database locator, tenant identity, bootstrap security policy |
| Environment override | deployment secret/configuration provider; process lifetime | no | DSNs, infrastructure endpoints, keyring material |
| Deployable application configuration | versioned tenant JSON or deployment artifact | no | queue topology, worker limits, provider selection |
| Tenant-editable setting | declared owner; changed through an authorized application use case | yes | integration enabled flag, UI preference, theme.selection, default/documentation locales |
| Runtime state/checkpoint | the operation that advances it; migration-backed and retention-aware | yes, only through its state repository | import jobs, cursors, health/checkpoint timestamps |
| Credential | declared setting or domain credential plus the core secret-store context | only through a write-only authorized use case | API key, token, TOTP secret |
| Taxonomy | a vocabulary/term/assignment domain with explicit target ownership | only if a separate taxonomy decision is accepted | none in core today |
The executable setting contract lives in src/Application/Settings:
SettingDefinitiondeclares owner, stable key, value type, default, executable constraints, scope, sensitivity, editability, and migration/deprecation data;SettingSchemaRegistryrejects duplicate keys and undeclared access;SettingResolveraccepts one source per layer and applies only the layers allowed by the definition scope;- tenant definitions accept only the tenant layer, deployable definitions accept deployable plus environment override, and bootstrap definitions accept bootstrap plus environment override;
TypedTenantSettingsis the compatibility boundary over the existingAppSettingsrepresentation. It normalizes reads/writes and delegates credentials to the existing AppSettings owner catalog andenc:v2store.
No resolver includes a runtime-state layer. Runtime state has a domain-specific
repository and migration instead of a new generic state bag. Existing durable
Psapi/Wpapi run stores remain module-owned. Esapi synchronization state has one
owner in esapi_sync_log; Firefly III jobs/checkpoints have one owner in
fireflyiiiapi_import_jobs.
OneUptimeApi is the first owner migrated to a complete typed schema. Further
owners migrate independently; raw AppSettings remains a compatibility API
during that process, while typed access rejects undeclared keys immediately.
The theme pair is a specialized early tenant setting: it shares one
app_settings row and an opaque revision with module-state writes, is read
before the container, and cannot be written through generic AppSettings::set().
i18n.default_locale joins that narrow early reader; Documentation's fallback
locale stays module-owned and loads after the container. The tenant JSON
database locator and i18n.supported_locales remain deployment policy.
Taxonomy Semantic Review
The reviewed models do not provide two consumers with the same vocabulary, term, assignment, translation, hierarchy, authorization, and deletion rules:
| Candidate | Actual semantics | Decision |
|---|---|---|
| Blog and Page | Neither native content model currently owns term assignments. Wpapi keeps source WordPress terms in connector bridge tables and does not establish a native Blog/Page taxonomy contract. | No consumer pair. |
| Kit tags | Flat, free-form JSON labels embedded in files and links for local filtering; no stable term identity or shared lifecycle. | Stay in Kit. |
| Finance categories/tags | Accounting classifications attached to transactions. Categories and colored tags have different relationships and finance-owned deletion behavior. | Stay in Finance. |
| Ticket categories | Required bounded string value on each ticket, used for operational filtering; there is no term repository or assignment entity. | Stay in Ticket. |
| Wpapi mappings | Source-system identity/provenance for WordPress editorial terms and WooCommerce catalog categories. Connector lifecycle follows remote ids. | Stay in Wpapi bridges. |
| Ecommerce categories | Hierarchical, localized, SEO-addressable catalog navigation with active/order/import-lock state and product/search/menu consequences. | Stay in Ecommerce. |
Core taxonomy tables, target-provider hooks, shared ACL, shared admin UI, and
consumer migrations are therefore rejected. They can be reconsidered only when
two named active consumers pass the core-promotion review in
doc/public/en/architecture/core-promotion.md.
Consequences
- Similar names no longer justify a shared persistence model.
- Tenant settings gain executable semantics incrementally without a flag-day rewrite or a second credential cipher.
- Runtime jobs and timestamps can have retention, concurrency, and audit rules appropriate to their operation.
- The legacy Firefly III last-sync timestamp is migrated into a synthetic completed job before obsolete runtime keys are removed.
- A future taxonomy proposal must supply two matching consumers and an explicit migration/export/rollback design; the current decision creates no core table.