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 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
Each layer overrides values from lower layers. For example:
daily_budget_usd = 5.0daily_budget_usd = 10.0 in ~/.qntx/config.tomldaily_budget_usd = 20.0 in project config.toml20.0 (project wins)/etc/qntx/config.toml)~/.qntx/config.toml)~/.qntx/config_from_ui.toml)./config.toml)QNTX_SECTION_KEY=valueQNTX_PULSE_DAILY_BUDGET_USD=50.0All UI changes write to ~/.qntx/config_from_ui.toml:
// Example: Toggle Ollama
config.UpdateLocalInferenceEnabled(true)
Implementation:
~/.qntx/config_from_ui.tomlBenefits:
Users edit ~/.qntx/config.toml directly:
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.
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:
system - From /etc/qntx/config.tomluser - From ~/.qntx/config.tomluser_ui - From ~/.qntx/config_from_ui.tomlproject - From ./config.tomlenvironment - From QNTX_* env vars// 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
}
// 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)
}
No migration required:
config.toml files work unchanged~/.qntx/config_from_ui.toml~/.qntx/config.toml): Comments preserved~/.qntx/config_from_ui.toml): Comments not preserved (auto-generated)Problem: Original design wrote UI changes to project config.toml, risking accidental git commits of user preferences.
Solution: Separate config_from_ui.toml ensures:
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).
Potential additions (not currently in scope):