Tenant JSON field disposition
Tenant JSON identifies a tenant, locates its database, and carries deployment
policy needed before tenant settings can be read. The configured database is not
the source for its own locator. TenantConfigInventory classifies raw JSON
paths; unknown paths fail the audit. The runtime TenantConfigRepository
resolves environment placeholders, follows aliases and normalizes absent
module_config to an empty array. Its optional disk cache stores only that
resolved JSON configuration and invalidates on source digest, mtime or
referenced environment changes. A DB-backed effective tenant snapshot must
remain separate from both raw JSON and its cache.
This matrix was checked against canonical raw files on 2026-09-14: seven
canonical configurations and one alias, with 54 distinct normalized leaf paths
after safe omissions and zero unclassified paths. Numeric list positions are
normalized to .*; an empty array or object contributes its parent path. Counts
refer to the repository snapshot, not a live tenant database. No hostname,
credential value or tenant-specific choice is recorded here.
| Raw path(s) | Owner and consumers | Precedence and absence behavior | Disposition |
|---|---|---|---|
id |
Tenant resolver, context and bootstrap | Required raw identity before DB access. | Retain. |
alias_of, redirect_to_canonical |
TenantConfigRepository host resolution |
Alias files are followed before canonical config is loaded; missing redirect means no forced canonical redirect. | Retain only in alias files; audit aliases separately. |
database.driver, database.provider, database.url, database.path, database.host, database.port, database.dbname, database.user, database.password, database.charset, database.sslmode |
Tenant DB connection factory and settings bootstrap | JSON or allowed environment placeholder determines the connection; no DB fallback can locate its own DB. Driver/path are present in every canonical file; other fields are connection-shape dependent. | Retain locator/topology and pre-DB credentials. Do not expose through backoffice. |
boot_profile |
TenantContext, ModuleBootstrapper, container and route caches |
Missing or invalid value falls back to full. |
Retain when selecting a non-default deployment footprint; an omitted default follows code. |
modules.* |
Module catalog, active module resolver, migration/install paths | Deployment allowlist is read before container/module states; DB module states govern requested activation within it. | Retain. The two full development tenant inventories remain complete. |
fixtures.enabled, fixtures.enabled.* |
TenantContext and fixture runner |
Missing or empty selection enables no optional fixture pack. | Retain nonempty install-time choices; empty selection can be omitted after install-flow equivalence. |
features.require_change_verification |
Security definitions for account verification | Missing value means verification required. | Retain explicit deployment safety policy, particularly intentional development exceptions. |
compilation.* |
TenantContext, config/container/route cache factories |
Absent compilation object disables compilation; optional subflags default within an enabled object. No canonical leaf currently uses it. | Retain as deployment-only when intentionally enabled. |
theme.front, theme.admin |
Early module compatibility and theme wrapper selection, render profile, email; coordinated DB theme selection is the target owner | Legacy JSON pair supplies migration fallback only while the aggregate row is absent. A valid DB aggregate wins; malformed present DB data is an error. | Removed from all seven configured JSON files after five rendering-tenant DB readbacks and two lite default-pair equivalence checks. The lite tenants exclude theme wrappers and have no theme aggregate. |
theme.overrides, theme.overrides.* |
Legacy route resolver and theme-wrapper synchronization interpret values differently | Empty object and absent key both yield no overrides. No canonical nonempty object was found. | Five empty objects removed. Preserve any external nonempty override pending separate contract review; no editor in this change. |
i18n.default_locale |
Locale middleware and HTTP, email/import/search consumers | JSON is a legacy fallback only if the core DB setting is absent; chosen locale must belong to supported locales. | Removed from all seven configured JSON files after DB readback. |
i18n.documentation_locale |
Documentation module fallback catalog | JSON is a legacy fallback only if the module-owned DB setting is absent; installed catalogs constrain values. | Removed from five Documentation-enabled tenant JSON files after DB readback. The two lite tenants exclude Documentation and had exact en compatibility defaults, so those redundant fields were omitted without DB rows. |
i18n.supported_locales.* |
RequestPrivacy, SideDetectionMiddleware, locale middleware and localized content owners |
Read before routing; missing consumers have differing code fallbacks, so omission is not equivalent. | Retain as deployment policy; changing it requires a separate privacy/routing contract. |
search.engine, search.sql.table, search.modern.provider, search.modern.enabled, search.modern.endpoint, search.modern.index, search.modern.index_per_tenant, search.fallback.enabled |
Search adapter selection, indexes and failover | Deployment topology and provider-specific defaults; absent modern settings do not imply a safe switch to another provider. | Retain technical topology. Review redundant fields only against every search consumer in a separate tranche. |
module_config.redis.host, module_config.redis.port, module_config.redis.db, module_config.redis.password |
Shared Redis connection | Process/worker transport connection; AiModuleConfig can fall back to this Redis object. |
Retain topology/credential fields. |
module_config.queue.name, module_config.queue.driver, module_config.queue.max_attempts, module_config.queue.redis.host, module_config.queue.redis.port, module_config.queue.redis.db, module_config.queue.redis.password, module_config.queue.redis.prefix, module_config.queue.worker.slot, module_config.queue.worker.sleep, module_config.queue.worker.max_idle, module_config.queue.worker.max_jobs, module_config.queue.worker.reclaim_after, module_config.queue.worker.reclaim_interval, module_config.queue.worker.reserve_timeout |
Queue transport, topology registry and worker CLI | Transport/worker topology is resolved at startup; several values have code defaults but an explicit deployment pin can be intentional. | Retain until a separately measured worker/topology default audit establishes omission intent. No general backoffice editor. |
module_config.ai.advanced.queue, module_config.ai.advanced.worker_heartbeat_max_age_seconds, module_config.ai.advanced.python.binary, module_config.ai.advanced.python.script, module_config.ai.advanced.python.working_directory, module_config.ai.advanced.python.timeout_seconds |
AI worker/process settings | Worker executable paths and limits apply at process startup. | Retain deployment/process policy. |
module_config.ai.redis, module_config.ai.redis.host, module_config.ai.redis.port |
AiModuleConfig transport selection |
An absent AI Redis object falls back to shared module_config.redis; an explicitly empty AI object selects AI's bounded host/port defaults instead. |
Retain explicit empty objects until the transport fallback semantics are reviewed. |
module_config.ai.history_limit, module_config.ai.chat.max_prompt_length, module_config.ai.upload.max_file_size_mb, module_config.ai.upload.max_extracted_characters, module_config.ai.rate_limit.reply_per_minute, module_config.ai.rate_limit.document_upload_per_minute, module_config.page.max_revisions |
AiModuleConfig, AiFeatureSettings, Page revision service |
All 35 previously present scalar values were exactly equal to the corresponding code fallbacks. Missing keys use those fallbacks in the same consumers. | Omitted from five canonical files. This intentionally follows bounded code defaults on future releases; reintroduce an explicit policy pin when a deployment must hold a particular ceiling. AI history count is prompt context, not stored-history retention; Page pruning can delete revisions. |
empty top-level module_config |
TenantConfigRepository normalization |
Missing and empty object both resolve to module_config: []. |
Two empty objects removed. |
The classification is a boundary, not permission to mutate every classified
field. Backoffice-editable settings use their declared owner, validation, ACL,
audit and DB storage. A legacy JSON fallback is read-only during cutover. After
the 2026-09-14 per-tenant readback and JSON cleanup, DB values are authoritative
for theme and locale choices in five rendering tenants, and default locale in
the two lite tenants; tenant identity, connection, module allowlist and early
safety policy remain deployment-owned.
Cutover verification on 2026-09-14: seven SQLite databases and eight JSON files
(including the alias) had private, integrity-checked backups; a restored copy
was opened successfully. Readback confirmed five theme aggregates, seven core
default locales and five Documentation locales before removing their JSON
counterparts. Fresh CLI diagnostics and raw-key audits passed for all seven
canonical tenants. An isolated restarted-process check rendered notification
email in the new DB default locale. A paired 100-iteration disposable-SQLite
bootstrap measurement found one connection and four SQL reads versus the prior
one state read; median times were 1.079 ms and 0.871 ms, respectively. These
local timings are indicative, not a deployment latency guarantee. PostgreSQL
portability remains untested in this PHP environment because pdo_pgsql is
unavailable.
To reproduce the raw path audit without printing configuration values:
php -r 'require "vendor/autoload.php"; $inventory = new K0smos\Tenant\TenantConfigInventory(); $paths = []; $unknown = []; $canonical = 0; $aliases = 0; foreach (glob("config/tenants/*.json") ?: [] as $file) { $raw = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); if (isset($raw["alias_of"])) { $aliases++; continue; } $canonical++; foreach ($inventory->audit($raw) as $row) { $paths[$row["path"]] = true; if ($row["category"] === "unknown") $unknown[$row["path"]] = true; } } printf("canonical=%d aliases=%d paths=%d unknown=%d\n", $canonical, $aliases, count($paths), count($unknown));'
tenant:config:audit reads the running tenant's raw canonical JSON through
findRawByHost(); it does not count synthetic DB overlay fields. The command
above is the reproducible repository-wide inventory, including alias counts.