Skip to main content

Overview

The HTTP transport (transports/vikat-http) never reads configuration from disk or database on the request path. At startup, lib.LoadConfig (in transports/vikat-http/lib/config.go) builds a single in-memory lib.Config struct that holds everything handlers need — client config, providers, MCP config, governance config, plugins — protected by read-write mutexes, with lock-free atomic.Pointer caches for the plugin hot path. That in-memory state is assembled from three sources:
  1. config.json — the file at <app-dir>/config.json (the directory passed via the -app-dir flag)
  2. The config store — a SQLite or PostgreSQL database (vikat.local/framework/configstore) that persists every change made through the HTTP API and UI
  3. Environment variables — referenced from either of the above via the env. prefix, plus a zero-config auto-detection path
Because both the file and the database can describe the same entity, boot performs a reconciliation pass per entity: each entity loaded from config.json is content-hashed, the hash is compared against the hash stored with the database row, and the winner is chosen from that comparison. The rule throughout is:
  • Hash matches → the file is unchanged since it was last synced, so the database row wins (this is what preserves edits made through the UI/API across restarts).
  • Hash differs → the file was edited, so the file wins and is written back to the database with the new hash.
  • Entity only in the file → it is added to the database.
  • Entity only in the database → it is kept (it was created via the API/UI) — unless source_of_truth is config.json (see below).
API/UI edits deliberately do not update the stored ConfigHash, which is why a hash match can be read as “the file has not changed” rather than “nothing has changed”.

config.json vs the config store

LoadConfig reads <app-dir>/config.json if it exists. A missing file is not an error — the gateway boots with defaults and whatever the config store already holds. If the file exists but its $schema value is not https://schema.vikat.ai (checked by isRecognizedConfigSchema), a warning is printed; the file is still parsed normally. The config store itself is configured by the config_store section of config.json, handled in initStores:
config.json
The SQLite variant takes {"path": "/path/to/config.db"} (configstore.SQLiteConfig). The Postgres fields are SecretVar values, so every one of them accepts an env.NAME reference. A logs store (logs_store section, default SQLite at <app-dir>/logs.db) and an optional vector store (vector_store) are initialized in the same pass.

Boot sequence

LoadConfig runs these steps in order (the numbering mirrors the code):
  1. EncryptioninitEncryption (uses encryption_key from the file, or the VIKAT_ENCRYPTION_KEY env var)
  2. StoresinitStores (config, logs, vector)
  3. KV store and feature flagsinitKVStore, initFeatureFlags
  4. Client configloadClientConfig (store → file → DefaultClientConfig)
  5. ProvidersloadProviders (store → file → env auto-detect)
  6. MCP configloadMCPConfig
  7. Governance configloadGovernanceConfig
  8. Auth configloadAuthConfig
  9. PluginsloadPlugins
  10. Skills registryloadSkillsRegistry
  11. Framework config and pricinginitFrameworkConfig
  12. Encryption sync, env label, WebSocket and server defaults
If the parsed file contains top-level keys that nothing in this build reads, ConfigData.UnrecognizedSections reports them and boot logs an ERROR stating those sections have been ignored — the file still parses and the gateway still starts.

Content-hash reconciliation per entity

Every reconcilable entity type has its own hash function in framework/configstore and its own matching rule: A detail that matters for secret rotation: GenerateKeyHash hashes an env.-referenced value as the reference string (ref:env.OPENAI_API_KEY), not the resolved value. Rotating the secret in the environment therefore does not register as config drift — the key keeps its DB identity and only the resolved value changes.

source_of_truth

The top-level source_of_truth field in config.json selects between two reconciliation modes (normalizeSourceOfTruth):
  • "split" (the default, and what any unknown value falls back to) — the merge behaviour described above. The file and the database each own what they created; DB-only entities are preserved.
  • "config.json" — sections that are present in the file become authoritative:
    • File entities are synced even when their hash matches the stored one, since a hash match cannot prove the DB row is unchanged (UI edits don’t bump the hash). This reverts UI drift back to file values on every restart.
    • Database rows not present in the file are deleted: providers and keys via syncAuthoritativeProvidersInStore, governance rows via pruneGovernanceConfigToFile, plugins via syncPluginsFromFile — each inside a store transaction.
    • Sections absent from the file are untouched, so you can put only providers under file control and keep managing governance from the UI. Presence is tracked per top-level key (sectionPresent) and per governance collection (governanceSectionPresent).

Environment variables

Environment variables enter the picture in three ways: env. references. Any SecretVar field — provider key values, store credentials, encryption_key, and others — accepts the string form "env.VAR_NAME" (see core/schemas/secretvar.go and framework/envutils.ProcessEnvValue). The reference is stored, the value is resolved from the process environment, and hashes and redacted API responses see only the reference. A vault. prefix is likewise reserved for vault-backed secrets. Provider auto-detection. When neither the file nor the database defines any provider, autoDetectProviders checks a fixed set of variables — OPENAI_API_KEY/OPENAI_KEY, ANTHROPIC_API_KEY/ANTHROPIC_KEY, MISTRAL_API_KEY/MISTRAL_KEY — and creates a provider per hit, with the key stored as an env. reference and models: ["*"]. The result is persisted to the config store, enabling zero-config startup. Direct reads. A few settings read the environment directly: VIKAT_ENV_LABEL (used only when the file’s env_label is empty), VIKAT_ENCRYPTION_KEY, and feature-flag file values, which accept {"enabled": "env.VIKAT_FOO"} indirection so Helm can flip flags without re-templating JSON. Feature-flag values declared in the file win over DB overrides and render as locked in the UI.

Runtime updates

After boot, mutations arrive through the HTTP API and go through methods on lib.ConfigAddProvider, UpdateProviderConfig, AddProviderKey, RemoveProvider, AddMCPClient, UpdateMCPClient, and so on. Each takes the write lock, updates the in-memory state, and persists to the config store in the same call. config.json is never rewritten by the gateway — which is exactly why the stored content hashes are needed to tell, on the next boot, whether the file or the database moved since they last agreed.