ADR-0019: Unified org_module_configs Table for Per-Org Module Configuration

Status

Accepted

Tags

fieldforce, org-config, jsonb, multi-tenancy, modules

Decision

Per-org module configuration is stored in a single org_module_configs(org_id, module, config JSONB) table with a unique index on (org_id, module). Each module owns its config shape as a JSONB blob. Missing rows return Go-hardcoded defaults. Platform-wide defaults are version-controlled in Go code, not a database table.

Why

Every new module that introduces org-configurable settings would otherwise require its own migration and table (org_fieldforce_configs, org_leave_configs, etc.). A unified table allows any module to store its config without schema changes — only a new module value is needed. The JSONB shape per module is validated by the Go service layer, not the database, keeping the schema open for evolution. Rejected alternatives:
  • Per-module tables: org_fieldforce_configs, org_ai_configs, etc. One migration per new module, no unified API, no consistent query pattern. org_ai_configs already exists with this pattern and stays as-is (it handles encrypted keys that require special treatment). Rejected for new modules.
  • EAV org_configs(org_id, key, value text): Maximum flexibility, zero type safety. Structured config values become awkward string blobs. Rejected.
  • Platform-editable defaults in DB (platform_module_configs table): Adds admin UI complexity with no urgency. Defaults belong in code where they are version-controlled and reviewed. Deferred.

How it works

Read flow with default merge:
Write flow (upsert):
Go-hardcoded defaults (not in DB):

Known limitations

  • Dual config patterns coexist: org_ai_configs (legacy, encrypted key column) and org_module_configs (new, general-purpose JSONB). Future cleanup should fold org_ai_configs into org_module_configs. Until then, two patterns exist in the codebase.
  • No DB-level schema enforcement: JSONB accepts any structure — an incorrect config shape silently stores garbage. The Go service layer is the only validation point. Future modules must implement their own validation on write.

Rules for agents

  • New modules adding org-configurable settings MUST use org_module_configs — do not create new per-module config tables
  • Go services MUST merge stored config with hardcoded defaults field-by-field — a partial row (org set only some fields) must return full defaults for unset fields
  • PUT .../configs/:module MUST be restricted to Admin/Owner role only
  • Never return raw JSONB to the client — always unmarshal, merge with defaults, and return a typed struct
  • org_ai_configs remains separate — do not attempt to move encrypted API keys into org_module_configs

Bad pattern (do not generate)

Good pattern