Configuring Local Development - ericfitz/tmi GitHub Wiki
This guide explains how to configure TMI for local development: where the config file lives, how secrets are managed, and how to authenticate against a running local server.
TMI configuration is split into two layers. Bootstrap config β database connection, JWT secret, server port, logging β comes from a config-*.yml file (plus TMI_* environment overrides) and is read once at startup. Operational config β OAuth providers, Timmy AI settings, timeouts, feature flags β is stored in the database, seeded from registry defaults on first run, and changed at runtime via the admin API without restarting the server. See Configuration-Model for the full picture.
# 1. Copy the template
cp config-example.yml config-development.yml
# 2. Open config-development.yml and replace the vault:// placeholders
# with real dev values (see "The dev config file" below)
# 3. Deploy the dev environment (CLUSTER is required on every dev-* target)
make dev-up CLUSTER=docker-desktop # or CLUSTER=k3s; DB=postgres|oracle (default postgres)
# 4. Verify the server is up
curl http://localhost:8080/make dev-up deploys the TMI server and the component platform (Redis, NATS, the controller, and the workers) into a local Kubernetes cluster, delivering config-development.yml as a ConfigMap, then port-forwards the server to port 8080. With DB=postgres (the default) it also deploys an in-cluster Postgres, and a postgres://user:pass@localhost⦠database.url is rewritten to that in-cluster service; remote URLs are left as written. See Local Development Cluster for prerequisites, Docker Desktop/k3s setup, database options (including DB=oracle, Docker Desktop only), and day-to-day commands.
Note: There is no
/healthendpoint. Use the root endpoint (/) to confirm the server is running.
| File | Tracked in git? | Purpose |
|---|---|---|
config-example.yml |
Yes | Template; shows all bootstrap keys with vault:// placeholders for secrets |
config-development.yml |
No (gitignored) | Your local working config; contains real secrets |
config-test.yml |
No (gitignored) | Used by the test suite |
config-production.yml |
No (gitignored) | Production deployments |
Never commit config-development.yml. It contains real credentials and is gitignored for that reason.
The config file carries only the bootstrap keys β settings the server must have before it can connect to the database:
-
database.urlβ PostgreSQL connection string (user, password, host, port, database name). This is where you look up your local dev DB credentials. -
server.port,server.tls_*β HTTP/TLS settings -
auth.jwt.secretβ Key used to sign and verify JWTs -
logging.*β Log level and format -
secrets.providerβ Where TMI should resolvevault://references at runtime
Everything operational (OAuth providers, Timmy, timeouts) is not present here β it lives in the database.
The vault:// placeholders in config-example.yml represent secret values. For local development you can:
- Inline real values β simplest for a dev machine only you control.
-
Use
env://references β e.g.env://MY_JWT_SECRETreads the value from that environment variable at startup. -
Use
file://references β e.g.file:///run/secrets/jwt_secretreads from a file.
For a local dev machine, inlining values is the most straightforward option.
If the bootstrap key registry changes (for example, after a config refactor), regenerate config-example.yml to pick up new keys:
make generate-config-exampleCommit the updated config-example.yml. Do not commit the working file.
Any bootstrap setting can be overridden with a TMI_ environment variable. Environment variables take highest priority β they win over both the config file and the database.
The naming convention is TMI_ followed by the YAML key path in uppercase with dots replaced by underscores:
# Override the server port without editing the config file
export TMI_SERVER_PORT=9090
# Override the database URL
export TMI_DATABASE_URL="postgres://user:pass@localhost:5432/tmidev"
# Override the JWT secret
export TMI_JWT_SECRET="my-local-dev-secret"This is useful for quick iteration (testing a different port, pointing at a different database) without touching config-development.yml.
TMI requires OAuth authentication for all protected endpoints. For local development, use the built-in "tmi" OAuth provider together with the OAuth callback stub.
make start-oauth-stubStub logs are written to /tmp/oauth-stub.log. Stop it with:
make stop-oauth-stub# Start the stub (if not already running)
make start-oauth-stub
# Start an automated OAuth flow for user "alice"
curl -X POST http://localhost:8079/flows/start \
-H 'Content-Type: application/json' \
-d '{"userid": "alice"}'
# Wait a moment for the flow to complete, then retrieve the token
curl "http://localhost:8079/creds?userid=alice" | jq '.access_token'Use the returned access_token as a Bearer token in your API requests:
curl -H "Authorization: Bearer <token>" http://localhost:8080/threat_modelsThe "tmi" provider supports a login_hint query parameter that creates a predictable test user instead of a random one:
http://localhost:8080/oauth2/authorize?idp=tmi&login_hint=alice
- Creates
[email protected]with display nameAlice (TMI User) - Format: 3β20 characters, alphanumeric plus hyphens, case-insensitive
- Without
login_hint, you get a random user like[email protected] -
login_hintis only available in the "tmi" provider β it does not exist in production builds
| User | Role | When to use |
|---|---|---|
charlie |
Administrator | Admin API calls, settings management |
alice |
Regular user | General development and testing |
bob |
Regular user | Multi-user scenarios |
On the first run against an empty database, TMI seeds all operational settings from the built-in registry defaults. You do not need to configure them manually to get a working server.
To inspect or change an operational setting after first run, use the /admin/settings admin API as the charlie admin user. For example, to check Timmy's configuration:
# Get a token for charlie
TOKEN=$(curl -s "http://localhost:8079/creds?userid=charlie" | jq -r '.access_token')
# List all settings (the endpoint takes no query parameters); filter for Timmy with jq
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/settings" | jq '[.[] | select(.key | startswith("timmy."))]'Timmy is the clearest example of a fully DB-backed subsystem: there is no timmy: block in the config file. To enable or configure Timmy in a local environment, update its timmy.* operational settings via the admin API. No server restart is needed β Timmy's access to content sources is toggled at runtime.
You can also inspect settings directly in the database β the /db skill runs queries against the local PostgreSQL instance (e.g. SELECT setting_key, value FROM system_settings WHERE setting_key LIKE 'timmy.%'). Editing should still go through the admin API so caching and encryption are handled correctly.
See Managing-Operational-Settings for a full guide to the admin settings API.
In development and test mode, TMI writes structured logs to:
logs/tmi.log
This file is in the project root directory. It is the first place to look when debugging unexpected server behavior. The log level is controlled by logging.level in config-development.yml or the TMI_LOG_LEVEL environment variable.
-
Configuration-Model β Deep dive into bootstrap vs. operational config and the
vault://secret reference format - Managing-Operational-Settings β How to inspect and update operational settings via the admin API
- Bootstrapping-Production β Equivalent guide for production deployments
- Getting-Started-with-Development β Cloning the repo, installing prerequisites, and the overall dev workflow
-
Local-Development-Cluster β The cluster-based
dev-uponramp: prerequisites, Docker Desktop/k3s, and database targets (incl.DB=oracle) - Database-Setup β Detailed PostgreSQL container setup and schema migration