Files
harmony/docs/concepts/configuration.md
2026-03-19 17:02:17 -04:00

5.1 KiB

Configuration and Secrets

Harmony treats configuration and secrets as a single concern. Developers use one crate, harmony_config, to declare, store, and retrieve all runtime data — whether it is a public hostname or a database password.

The mental model: schema in Git, state in the store

Schema

In Harmony, the Rust code is the configuration schema. You declare what your module needs by defining a struct:

#[derive(Config, Serialize, Deserialize, JsonSchema, InteractiveParse)]
struct PostgresConfig {
    pub host: String,
    pub port: u16,
    #[config(secret)]
    pub password: String,
}

This struct is tracked in Git. When a branch adds a new field, Git tracks that the branch requires a new value. When a branch removes a field, the old value in the store becomes irrelevant. The struct is always authoritative.

State

The actual values live in a config store — by default, OpenBao. No .env files, no JSON, no YAML in the repository.

When you run your code, Harmony reads the struct (schema) and resolves values from the store (state):

  • If the store has the value, it is injected seamlessly.
  • If the store does not have it, Harmony prompts you in the terminal. Your answer is pushed back to the store automatically.
  • When a teammate runs the same code, they are not prompted — you already provided the value.

How branch switching works

Because the schema is just Rust code tracked in Git, branch switching works naturally:

  1. You check out feat/redis. The code now requires RedisConfig.
  2. You run cargo run. Harmony detects that RedisConfig has no value in the store. It prompts you.
  3. You provide the values. Harmony pushes them to OpenBao.
  4. Your teammate checks out feat/redis and runs cargo run. No prompt — the values are already in the store.
  5. You switch back to main. RedisConfig does not exist in that branch's code. The store entry is ignored.

Secrets vs. standard configuration

From your application code, there is no difference. You always call harmony_config::get_or_prompt::<T>().

The difference is in the struct definition:

// Standard config — stored in plaintext, displayed during prompting.
#[derive(Config)]
struct ClusterConfig {
    pub api_url: String,
    pub namespace: String,
}

// Contains a secret field — the entire struct is stored encrypted,
// and the password field is masked during terminal prompting.
#[derive(Config)]
struct DatabaseConfig {
    pub host: String,
    #[config(secret)]
    pub password: String,
}

If a struct contains any #[config(secret)] field, Harmony elevates the entire struct to ConfigClass::Secret. The storage backend decides what that means in practice — in the case of OpenBao, it may route the data to a path with stricter ACLs or audit policies.

Authentication and team sharing

Harmony uses Zitadel (hosted at sso.nationtech.io) for identity and OpenBao (hosted at secrets.nationtech.io) for storage.

First run on a new machine:

  1. Harmony detects that you are not logged in.
  2. It prints a short code and URL to your terminal, and opens your browser if possible.
  3. You log in with your corporate identity (Google, GitHub, or Microsoft Entra ID / Azure AD).
  4. Harmony receives an OIDC token, exchanges it for an OpenBao token, and caches the session locally.

Subsequent runs:

  • Harmony silently refreshes your tokens in the background. You do not need to log in again for up to 90 days of active use.
  • If you are inactive for 30 days, or if an administrator revokes your access in Zitadel, you will be prompted to re-authenticate.

Offboarding:

Revoking a user in Zitadel immediately invalidates their ability to refresh tokens or obtain new ones. No manual secret rotation is required.

Resolution chain

When Harmony resolves a config value, it tries sources in order:

  1. Environment variable (HARMONY_CONFIG_{KEY}) — highest priority. Use this in CI/CD to override any value without touching the store.
  2. Config store (OpenBao for teams, local file for solo/offline use) — the primary source for shared team state.
  3. Interactive prompt — last resort. Prompts the developer and persists the answer back to the store.

Schema versioning

The Rust struct is the single source of truth for what configuration looks like. If a developer renames or removes a field on a branch, the store may still contain data shaped for the old version of the struct. When another developer who does not have that change runs the code, deserialization will fail.

In the current implementation, this is handled gracefully: a deserialization failure is treated as a miss, and Harmony re-prompts. The new answer overwrites the stale entry.

A compile-time migration mechanism is planned for a future release to handle this more rigorously at scale.

Offline and local development

If you are working offline or evaluating Harmony without a team OpenBao instance, the StoreSource falls back to a local file store at ~/.local/share/harmony/config/. The developer experience is identical — prompting, caching, and resolution all work the same way. The only difference is that the state is local to your machine and not shared with teammates.