---
name: navree-config
description: "Configures a Navree app: `.env`, `config/app.toml` and `APP_*` environment variables and their precedence, the application key and its rotation, typed `ConfigSection`s with validation and `Secret<T>`, reading `Config`, custom sources and configuration in tests. Use when adding a setting or a config section for a module, reading an environment variable, setting APP_KEY, keeping secrets out of logs, or when a setting doesn't take effect."
---

# Configuration

Every setting lives in one tree, built at boot from sources; each module
reads its section as a typed struct. A wrong type or a failed validation
stops the boot with a message naming the section (never the value).

## Where settings come from

`src/main.rs` passes `ConfigSources::defaults()`, which reads, in order:

1. `.env` (this machine's secrets; never committed; commit `.env.example`),
2. `config/app.toml` (shared settings; committed; no secrets),
3. the process's `APP_*` environment variables.

**Later sources win.** So `.env` only fills keys that `config/app.toml`
leaves out: a new app's TOML sets `http.addr`, `http.api_prefix`, `database.url`,
`database.migrate_on_boot`, `log.level`, `mail.transport`, `queue.driver`,
`auth.registration` and `api.title`, and changing those in `.env` has no
effect. Edit the TOML file, remove the key from it, or use a real
environment variable. Paths are relative to the working directory: run the
app from its root.

## Naming

`APP_` + section + `__` + key, upper case: `APP_DATABASE__URL` is
`database.url`, `APP_HTTP__CORS__ALLOWED_ORIGINS` is
`http.cors.allowed_origins`, `APP_KEY` is the app key. A single `_` stays
in the name (`APP_HTTP__PER_IP_RATE_LIMIT`). Values are strings converted by
the section's types; settings that take a list also accept a comma-separated
string (`APP_HTTP__TRUSTED_PROXIES=127.0.0.1,::1`). Navree reads `.env`
itself and never copies it into the process environment, so
`std::env::var` doesn't see `.env` entries.

## The application key

```sh
navree key:generate     # prints base64:<32 random bytes>; put it in .env as APP_KEY
```

It encrypts data (AES-256-GCM: two-factor secrets, session cookies) and is
the base of the derived keys for JWTs (unless `auth.jwt.secret`), signed URLs
(unless `http.signing_key`) and the realtime relay (unless
`realtime.secret`). Every process of the app needs the same key. Without
one the app boots with a warning (so `key:generate` works), but encryption
fails and tokens/links use a random key per process. To rotate, move the old
key to `APP_PREVIOUS_KEYS` (comma-separated, newest first) and set the new
one: previous keys only decrypt; derived keys (JWTs, signed links) change,
so users sign in again.

## Your own section

```rust
use navree::config::ConfigSection;
use navree::contracts::Secret;
use serde::Deserialize;

/// The `billing` section (`[billing]` in config/app.toml, `APP_BILLING__*`).
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]                       // a missing section or key takes the defaults
pub struct BillingOptions {
    pub currency: String,
    pub trial_days: u32,
    pub api_key: Option<Secret<String>>, // APP_BILLING__API_KEY; prints [redacted]
}

impl Default for BillingOptions {
    fn default() -> Self {
        Self { currency: "EUR".to_owned(), trial_days: 14, api_key: None }
    }
}

impl ConfigSection for BillingOptions {
    const KEY: &'static str = "billing";

    fn validate(&self) -> Result<(), String> { // a failure stops the boot
        if self.currency.len() == 3 {
            Ok(())
        } else {
            Err("currency must be a 3-letter ISO code".to_owned())
        }
    }
}
```

Register it in the module (`m.config::<BillingOptions>()` in `wire`, from
the prelude's `ModuleBuilderConfigExt`), then inject
`Arc<BillingOptions>` into the module's providers. Read a secret with
`.expose()`. To let callers pass options in code too, write a configurable
module (`BillingModule::for_root(options)`; see `navree-modules-and-di`).

## Reading values directly

Inject `Arc<navree::config::Config>`: `config.get_string("app.url")`,
`config.get_bool("feature.x")`, `config.section::<T>("key")`,
`config.section_or_default::<T>("key")`, `config.typed::<T>()` (a
`ConfigSection`, validated). Its `Debug` output masks keys containing
`password`, `secret`, `token`, `key`, `authorization` or `cookie`, and URL
passwords: give secrets secret-looking names.

## Custom sources and environments

There is no `APP_ENV` switch. Add a per-environment file yourself:

```rust
use navree::config::ConfigSources;

pub fn sources() -> ConfigSources {
    let env = std::env::var("APP_ENV").unwrap_or_else(|_| "local".to_owned());
    ConfigSources::new()
        .dotenv(".env")
        .file("config/app.toml")
        .file(format!("config/{env}.toml")) // skipped when missing
        .env()
}
```

`.set(key, value)` applies one value above every source;
`.env_prefixed("MYAPP")` reads another prefix. Release builds default to
JSON logs and no template reloading; debug builds to pretty logs and reloading.

## Framework sections

`app` (`key`, `previous_keys`, `url`), `http` (`addr`, `api_prefix`,
`trusted_proxies`, `per_ip_rate_limit`, `body_limit_bytes`, `timeout_secs`,
`cors.allowed_origins`, `security_headers`), `log`, `hashing`, `validation`,
`database`, `auth`, `audit`, `cache`, `queue`, `mail`, `notify`, `storage`,
`http_client`, `realtime`, `view`, `api`, `observe`, `session`. Each
module's skill lists its keys.

## In tests

`TestApp::builder()` reads **no** sources (no `.env`, no TOML, no
environment). Set what the test needs:

```rust
let app = TestApp::builder()
    .config("billing.trial_days", 30)
    .module(BillingModule)
    .build()
    .await;
assert_eq!(app.get::<InvoiceService>().await.trial_days(), 30);
```

To boot the whole app, build sources with `ConfigSources::new().set(..)`
(not `defaults()`, which reads the machine's `APP_*` variables).

## Common mistakes

- Expecting `.env` to override `config/app.toml`.
- Secrets in `config/app.toml` (committed): use `.env` or the environment,
  and `Secret<T>` in your section.
- A section struct without `#[serde(default)]` and a missing key: the boot fails.
- Different `APP_KEY`s for `serve`, `queue:work` and `schedule:work`.
- `http.per_ip_rate_limit = 0` (fails the boot): only `null` (in code or
  test config) turns it off.

## Docs

- https://navree.com/docs/getting-started/configuration/
- https://navree.com/docs/security/encryption-and-hashing/
