Configuration System Architecture

Overview

QNTX uses a layered configuration system with five sources, merged with clear precedence rules. This design allows system-wide defaults, user preferences, team settings, and environment overrides to coexist cleanly. For the UI implementation of this system, see Config Panel. For the REST API, see Configuration API.

Configuration Sources

Configuration is loaded from multiple sources in this order (lowest to highest precedence):

1. System      /etc/qntx/config.toml               # System-wide defaults
2. User        ~/.qntx/config.toml                 # User manual configuration
3. User UI     ~/.qntx/config_from_ui.toml         # UI-managed configuration
4. Project     ./config.toml                       # Project/team configuration
5. Environment QNTX_* environment variables        # Runtime overrides

Source Precedence

Each layer overrides values from lower layers. For example:

File Responsibilities

System Config (/etc/qntx/config.toml)

User Config (~/.qntx/config.toml)

UI Config (~/.qntx/config_from_ui.toml)

Project Config (./config.toml)

Environment Variables

Config Update Strategy

UI Updates (Web Interface)

All UI changes write to ~/.qntx/config_from_ui.toml:

// Example: Toggle Ollama
config.UpdateLocalInferenceEnabled(true)

Implementation:

  1. Load existing UI config (or create if missing)
  2. Update specific field
  3. Marshal entire config struct to TOML
  4. Create backup (.back1, .back2, .back3 rotation)
  5. Write to ~/.qntx/config_from_ui.toml

Benefits:

Manual Updates

Users edit ~/.qntx/config.toml directly:

Source Tracking (Introspection)

Why Introspection?

SRE approach to configuration. Multi-source config creates observability problems. When something doesn't work, you need to know why.

Debugging: "Why isn't my config working?" User toggles Ollama in UI, nothing happens. Introspection shows project config is overriding user_ui. Now they know what to fix.

Trust/transparency: Without visibility, UI changes feel broken. Introspection proves changes took effect (or shows what's overriding them).

Security audit: See if environment vars are leaking into places they shouldn't. Know what's coming from where.

How It Works

The introspection endpoint (/api/config) shows where each value comes from:

{
  "local_inference": {
    "enabled": {
      "value": true,
      "source": "user_ui",        // From ~/.qntx/config_from_ui.toml
      "type": "bool"
    },
    "model": {
      "value": "llama3.2:3b",
      "source": "project",         // From ./config.toml
      "type": "string"
    }
  }
}

Source values:

Implementation Details

Config Loading

// Internal/config/config.go
func LoadConfig() (*Config, error) {
    // 1. Parse each source separately
    systemCfg := parseConfig("/etc/qntx/config.toml")
    userCfg := parseConfig("~/.qntx/config.toml")
    uiCfg := parseConfig("~/.qntx/config_from_ui.toml")
    projectCfg := parseConfig("./config.toml")

    // 2. Build source map (track where each value comes from)
    sources := buildSourceMap(systemCfg, userCfg, uiCfg, projectCfg)

    // 3. Merge with precedence
    merged := mergeConfigs(systemCfg, userCfg, uiCfg, projectCfg)

    // 4. Apply environment overrides
    applyEnvOverrides(merged)

    return merged, nil
}

Config Persistence

// Internal/config/persist.go
func UpdateLocalInferenceEnabled(enabled bool) error {
    // Get UI config path
    path := GetUIConfigPath()  // ~/.qntx/config_from_ui.toml

    // Load or initialize
    config, err := loadOrInitializeUIConfig()
    if err != nil {
        return err
    }

    // Update field (type-safe)
    config.LocalInference.Enabled = enabled

    // Save with backups
    return saveUIConfig(path, config)
}

Migration and Compatibility

Existing Configs

No migration required:

Comment Preservation

Design Rationale

Why Separate UI Config?

Problem: Original design wrote UI changes to project config.toml, risking accidental git commits of user preferences.

Solution: Separate config_from_ui.toml ensures:

  1. UI changes never touch project config
  2. Project config remains team-shared
  3. User preferences stay local

Why TOML Marshaling vs Regex?

Original approach: Regex pattern matching to preserve comments:

re := regexp.MustCompile(`(?sm)(\[local_inference\][^\[]*?^enabled\s*=\s*)(true|false)(.*)`)
updated := re.ReplaceAllString(content, fmt.Sprintf("${1}%s${3}", newValue))

Problems:

Current approach: Proper TOML marshaling:

config.LocalInference.Enabled = enabled
toml.Marshal(config)

Benefits:

Tradeoff: Comments in UI config are lost (acceptable for auto-generated file).

Related Documentation

Future Enhancements

Potential additions (not currently in scope):

  1. Config validation: Warn about invalid values before save
  2. Reset to defaults: UI button to clear user_ui config
  3. Export merged config: Download effective configuration
  4. Config diff view: Show what's different from defaults
  5. Multi-environment support: Dev/staging/prod profiles