ADR-023: Config-Driven Settings Engine for AppCMS

Prev Next

Context

AppCMS settings span 34 namespaces, each containing a variable number of form fields with heterogeneous types, conditional visibility rules, and grouped layouts. The prior approach required each namespace to maintain its own bespoke page component, resulting in duplicated form wiring, inconsistent layout, and high cost-per-namespace for onboarding new settings.

A decision was needed on how to render settings UI from configuration rather than hand-authored page components so that adding a namespace becomes a data concern, not a UI engineering effort.

Decision

Adopt a config-driven render engine as the single rendering path for all AppCMS settings namespaces.

  1. Typed config per namespace — Each namespace declares a SectionConfig, a typed JSON-compatible structure that groups fields into SectionItem blocks.

  2. Normalizing adapternormalizeRawConfig converts raw API config payloads into the typed SectionConfig shape, inferring field types from value shapes when explicit types are absent.

  3. Field-component registry — The render engine delegates each field to a registry (FieldType → React component) supporting eight types: switch, text, number, textarea, select, combo, dataMap, and divider.

  4. 12-column grid layout — Layout is expressed via CSS grid with per-item span controls. Conditional visibility is resolved at render time from live form values using show path expressions evaluated via lodash/get.

  5. Lazy loading — Namespace configs are loaded through a lazy registry where each entry is a () => import(...) loader, so webpack code-splits every config into its own chunk. Config resolution and settings data fetch are parallelised at the consumer.

Image

Consequences

  • Enables

    • Adding a new namespace by authoring a single config file and one registry line — no new React components required
    • Deterministic, testable rendering: adapter normalization and conditional visibility are pure functions with full unit coverage
    • Bundle efficiency: only the config chunk for the opened namespace is loaded
    • Consistent layout and interaction patterns across all 34 namespaces without per-namespace divergence
  • Restricts

    • All settings UI must flow through the render engine; namespace-specific one-off components are not permitted
    • New field types require a registry entry and a corresponding field component before they can be used in configs
    • Layout is constrained to the 12-column grid model; freeform positioning is intentionally unsupported
  • Trade-offs accepted

    • Highly custom namespace UIs (if ever needed) would require extending the engine rather than bypassing it
    • The normalizing adapter carries inference heuristics that may need updating if backend config shapes evolve significantly

Notes

  • Related module docs: docs/modules/uploader/ (follows a similar config-driven pattern for upload workflows)
  • The adapter and render engine are covered by dedicated Jest suites under src/__tests__/
  • Namespace rollout is incremental — the registry uses Partial<Record> so unconfigured namespaces gracefully resolve to null