> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vikat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# In-Memory Config Store

> How the HTTP transport builds its in-memory configuration at boot — config.json, the config store database (SQLite/PostgreSQL), and environment variables merged with per-entity content-hash reconciliation.

## 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_store` section          | Behaviour                                                                                                                                |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Absent                          | A default SQLite store is created at `<app-dir>/config.db`                                                                               |
| Present with `"enabled": true`  | The declared store is used (`"type": "sqlite"` or `"postgres"`)                                                                          |
| Present with `"enabled": false` | No config store at all — nothing persists, and subsystems that need persistence (e.g. MCP client management) are disabled with a warning |

```json config.json theme={null}
{
  "$schema": "https://schema.vikat.ai",
  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.DB_HOST",
      "port": "5432",
      "user": "vikat",
      "password": "env.DB_PASSWORD",
      "db_name": "vikat",
      "ssl_mode": "require"
    }
  }
}
```

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. **Encryption** — `initEncryption` (uses `encryption_key` from the file, or the `VIKAT_ENCRYPTION_KEY` env var)
2. **Stores** — `initStores` (config, logs, vector)
3. **KV store and feature flags** — `initKVStore`, `initFeatureFlags`
4. **Client config** — `loadClientConfig` (store → file → `DefaultClientConfig`)
5. **Providers** — `loadProviders` (store → file → env auto-detect)
6. **MCP config** — `loadMCPConfig`
7. **Governance config** — `loadGovernanceConfig`
8. **Auth config** — `loadAuthConfig`
9. **Plugins** — `loadPlugins`
10. **Skills registry** — `loadSkillsRegistry`
11. **Framework config and pricing** — `initFrameworkConfig`
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:

| Entity                                                                                                            | Hash function                                                                                                          | Matched by                                                                                       | Notes                                                                                                                                                                            |
| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client config                                                                                                     | `GenerateClientConfigHashWithToolManager`                                                                              | singleton                                                                                        | Hash covers the `client` section **and** `mcp.tool_manager_config`, so a UI change to either survives restarts. A legacy stored hash (client section only) is upgraded in place. |
| Provider                                                                                                          | `ProviderConfig.GenerateConfigHash`                                                                                    | provider name (lowercased)                                                                       | Hash excludes keys — they are hashed separately.                                                                                                                                 |
| Provider key                                                                                                      | `GenerateKeyHash`                                                                                                      | stored key `ConfigHash`, falling back to key name                                                | On file change the DB key's `ID`, `Status` and `Description` are preserved (`mergeProviderKeys` / `reconcileProviderKeys`).                                                      |
| MCP client                                                                                                        | `GenerateMCPClientHash`                                                                                                | client `Name`                                                                                    | Handled in `mergeMCPConfig`.                                                                                                                                                     |
| Governance: budgets, rate limits, customers, teams, virtual keys, model configs, routing rules, pricing overrides | `GenerateBudgetHash`, `GenerateRateLimitHash`, `GenerateCustomerHash`, `GenerateTeamHash`, `GenerateVirtualKeyHash`, … | `ID` (customers and teams declared without an ID also match by `Name` and adopt the DB row's ID) | Handled per-collection in `mergeGovernanceConfig`.                                                                                                                               |
| Skills registry entry                                                                                             | `generateSkillRegistryEntryHash` (in `config_skills.go`)                                                               | skill name                                                                                       | Hash match skips file conversion and validation entirely.                                                                                                                        |
| Plugins                                                                                                           | *(no content hash)*                                                                                                    | plugin `Name`                                                                                    | A file plugin replaces the stored one when its `version` is higher or its placement/order changed (`mergePlugins`).                                                              |

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.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.
