Skip to content

Configuration Model

Eric Fitzgerald edited this page May 22, 2026 · 1 revision

Configuration Model

TMI configuration is divided into two distinct categories: bootstrap and operational. Understanding this split is the single most important concept for operating or developing against TMI. Bootstrap settings must be present before the server can start; operational settings live in the database and can be changed while the server is running.

This page explains the model. For task-specific guides, see Configuring-Local-Development, Bootstrapping-Production, Managing-Operational-Settings, and Configuration-Reference.


The Two Categories

Property Bootstrap Operational
Where stored Config file and/or environment variables system_settings database table
When read Once, at startup At use time (hot) or once at startup (static)
How to change Edit file or env var, then restart PUT /admin/settings/{key} — no restart needed for hot settings
Who seeds initial values Built-in defaults, overridden by file/env Built-in registry defaults written to DB on first run
Secrets handling Inline literals or secret references (vault/env/file) Encrypted at rest in the DB; rotated via /admin/settings/reencrypt
Example keys database.url, auth.jwt.secret, server.port OAuth providers, SAML providers, Timmy settings, feature flags

The split is not arbitrary — it reflects a chicken-and-egg reality. You cannot store the database connection string inside the database. Any setting that the server needs in order to reach its data stores must be a bootstrap setting.


How Values Are Resolved

Bootstrap settings

Built-in default  →  config-*.yml file  →  TMI_* environment variable
                                                         ↑
                                               (highest precedence)

Resolution happens exactly once, at startup. Environment variables always win. If a required setting has no effective value after resolution, Config.ValidateRequired() aborts the boot with a clear error naming the missing key.

Operational settings

First run (system_settings table is empty):
  Built-in registry defaults  →  written to system_settings table

At read time (env > file > db precedence):
  TMI_* environment variable                  ← wins if present
        ↓  else
  config-*.yml value                          ← wins if present
        ↓  else
  system_settings table  →  runtime-editable value (the common case)

Registry defaults seed the system_settings table on first run, and that table is the runtime-editable store you change through /admin/settings. However, the env > file > db precedence is still live and it applies to operational keys too. On every read the settings service checks its config provider (which merges environment variables over config-file values) first; only if no env/config value is present for the key does it fall back to the database value.

In the common case an operational key has no env var and no config-file entry, so the database value wins and is what you edit at runtime. But if an operator has set that key via a TMI_* environment variable or a YAML entry, that value overrides the database at read time, and PUT /admin/settings/{key} returns 409 Conflict with a message like Setting '<key>' is controlled by <source> and cannot be modified via the API.

Think of it as two independent rules working together: the category decides where a value is allowed to live (bootstrap keys are file/env only — the settings service refuses to serve them from the database; operational keys live in system_settings), and the precedence decides which value wins when more than one source supplies a value for the same key.

Pinning an operational setting. Because env/config wins over the database, you can deliberately "pin" any operational setting to a fixed value by declaring it in a TMI_* environment variable or a config file. This is useful for locking a value into a reproducible deployment. The trade-off: while it is pinned, that key cannot be edited through the admin API (it returns 409 Conflict) until you remove the env/config value, at which point the database value takes over again.


Secret References

A bootstrap setting that holds a secret does not have to store the secret inline. TMI supports three reference schemes that are resolved at startup:

Scheme Format What happens
Vault vault://PATH Secret is fetched from the configured secrets provider (secrets.provider)
Environment variable env://NAME The value of environment variable NAME is used
File file://PATH The whitespace-trimmed contents of the file at PATH are used
Inline (no prefix) The value is used as-is

Any value that does not begin with a recognized scheme prefix is treated as an inline literal.

Example bootstrap config file showing all four forms:

auth:
  jwt:
    secret: vault://secret/tmi/jwt-secret        # fetched from Vault at startup
    signing_method: HS256                          # inline literal

database:
  url: env://DATABASE_URL                         # read from environment variable
  redis:
    password: file:///run/secrets/redis-password  # read from a mounted secret file

secrets:
  provider: vault

The secrets.provider key selects which backend handles vault:// references. Supported providers: env, aws, azure, gcp, oci, vault.


Secrets Are Not All Bootstrap

"Secret" and "Bootstrap" are orthogonal properties. Confusing them leads to misconfiguration.

  • A bootstrap secret is a secret that must be known before the server can start (e.g., the DB password). It is resolved from env/file/vault at startup and never stored in the database.
  • An operational secret is a secret that is stored encrypted in the system_settings table, managed through the admin API, and rotated via /admin/settings/reencrypt. It never appears in a config file or environment variable.

Bootstrap secrets (examples)

Key Why it must be bootstrap
database.url Needed to open the database connection
auth.jwt.secret Needed to verify JWTs before any request can be authenticated
database.redis.password Needed to connect to Redis

Operational secrets (examples)

Key Where it lives
OAuth provider client_secret Encrypted in system_settings
timmy.llm_api_key Encrypted in system_settings
timmy.text_embedding_api_key Encrypted in system_settings
timmy.code_embedding_api_key Encrypted in system_settings
timmy.rerank_api_key Encrypted in system_settings

Timmy as a worked example

Timmy (the AI assistant) is entirely operational. There is no timmy: block in any bootstrap config file, and no TMI_TIMMY_* environment variables are required. Every timmy.* key — including the model selection, the provider, all API keys, and all tuning knobs — lives in system_settings.

This means:

  • You can enable or disable Timmy at runtime through the admin API without restarting the server.
  • You can rotate Timmy API keys through /admin/settings/reencrypt without touching config files.
  • A fresh development instance with no Timmy-related environment variables will pick up Timmy's configuration from the database defaults seeded on first run.

The broader principle: do not assume that because a setting holds a sensitive value, it must be a bootstrap setting. Check Configuration-Reference for each key's actual category.


Hot vs. Static Settings

Operational settings have a mutability property that tells you what happens when you change a value via the admin API.

Mutability Behavior
hot The new value is read at the next use. No restart required.
static The value was read once at boot. A restart is required for the change to take effect.

If you edit a static operational setting via /admin/settings, the database row is updated immediately, but the running server continues using the value it loaded at startup until you restart it. The Configuration-Reference page lists the mutability of each setting.


Visibility

Each setting has a visibility that controls where and to whom the value is exposed:

Visibility Where it appears
internal Server-side only. Never included in any API response.
admin-only Visible to administrators via the /admin config endpoints.
public Exposed on the unauthenticated discovery/config endpoint. Examples: operator name, OAuth provider display data, server.base_url.

A setting cannot be both public and a secret. This constraint is enforced at build time by a validation suite — if a registry entry is marked both public and secret, the server will refuse to start. This prevents accidental secret exposure through the discovery endpoint.


The Configuration Files

Bootstrap-only files

Three files hold bootstrap configuration. Each targets a deployment environment:

File Environment
config-development.yml Local development
config-test.yml Automated test runs
config-production.yml Production deployment

These files contain only bootstrap keys. Operational settings do not belong here and will be ignored if placed here.

All three working files are gitignored because they contain real secrets (database passwords, JWT signing secrets, etc.). Never commit them.

Generated example

config-example.yml is generated from the classification registry by make generate-config-example. It contains all bootstrap keys with vault:// placeholders for secrets. Use it as a template when setting up a new environment:

cp config-example.yml config-development.yml
# Then replace the vault:// placeholders with real values or other reference schemes

Database backend selection

The database backend is selected automatically from the URL scheme of database.url:

URL scheme Backend
postgres:// PostgreSQL
oracle:// Oracle Autonomous Database
sqlite:// SQLite
mysql:// MySQL
sqlserver:// SQL Server

See Database-Setup for backend-specific setup instructions.


Required Settings

Some bootstrap settings are marked Required in the registry. If any required setting has an empty effective value after the bootstrap cascade resolves, Config.ValidateRequired() aborts startup with a clear error that names the missing key.

Required settings include database.url, auth.jwt.secret, and server.port. You will not get a silently mis-configured server — a missing required value is a hard startup failure.


Flow Diagram

BOOTSTRAP settings
──────────────────
At rest:     config-*.yml  +  TMI_* env vars
             (working files are gitignored; secrets may be vault/env/file references)

At startup:  built-in default
                 ↓  overridden by
             config-*.yml value
                 ↓  overridden by
             TMI_* environment variable   ← wins
                 ↓
             secret reference resolved (vault:// / env:// / file://)
                 ↓
             ValidateRequired() — abort if any required key is empty
                 ↓
             value held in memory for server lifetime

OPERATIONAL settings
────────────────────
At rest:     system_settings table (secrets encrypted at rest)

First run:   built-in registry defaults  →  written to system_settings

At read:     TMI_* environment variable     ← wins if present  ┐
                 ↓  else                                        │ env > file > db
             config-*.yml value             ← wins if present  │ precedence
                 ↓  else                                        ┘
             system_settings table          ← the common case; runtime-editable

             hot setting     →  precedence re-evaluated on each access
             static setting  →  precedence evaluated at boot, held in memory
             (edit DB value via PUT /admin/settings/{key} — returns 409 if the key
              is pinned by an env/config value; rotate secrets via /admin/settings/reencrypt)

See Also

Home

Releases


Getting Started

Deployment

Operation

Troubleshooting

Development

Integrations

Tools

API Reference

Reference

Clone this wiki locally