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:
config.json— the file at<app-dir>/config.json(the directory passed via the-app-dirflag)- The config store — a SQLite or PostgreSQL database (
vikat.local/framework/configstore) that persists every change made through the HTTP API and UI - Environment variables — referenced from either of the above via the
env.prefix, plus a zero-config auto-detection path
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_truthisconfig.json(see below).
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
{"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):
- Encryption —
initEncryption(usesencryption_keyfrom the file, or theVIKAT_ENCRYPTION_KEYenv var) - Stores —
initStores(config, logs, vector) - KV store and feature flags —
initKVStore,initFeatureFlags - Client config —
loadClientConfig(store → file →DefaultClientConfig) - Providers —
loadProviders(store → file → env auto-detect) - MCP config —
loadMCPConfig - Governance config —
loadGovernanceConfig - Auth config —
loadAuthConfig - Plugins —
loadPlugins - Skills registry —
loadSkillsRegistry - Framework config and pricing —
initFrameworkConfig - Encryption sync, env label, WebSocket and server defaults
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 inframework/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 viapruneGovernanceConfigToFile, plugins viasyncPluginsFromFile— each inside a store transaction. - Sections absent from the file are untouched, so you can put only
providersunder 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 onlib.Config — AddProvider, 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.
