navree

Configuration

Typed configuration from .env, config/app.toml and APP_* environment variables: precedence, nesting, the application key and your own sections.

On this page

Every setting of a Navree app lives in one configuration tree. The app builds it at boot from a .env file, config/app.toml and APP_* environment variables, and each module reads its own section of it as a typed Rust struct. A missing setting falls back to the module's default, and a setting of the wrong type, or one its module rejects, stops the boot with a message that names the section.

navree-config does the loading, and ConfigModule makes the result available to every other module.

Where settings come from

A generated app's main passes the conventional sources to the app root:

src/main.rs

ConfigSources::defaults() reads, in this order:

  1. .env
  2. config/app.toml
  3. the process's APP_* environment variables

Later sources win. Tables merge key by key, so a later source only replaces the keys it sets; any other value (a string, a number, an array) is replaced whole. The paths are relative to the working directory, so run the app from its root. A missing file is skipped, and a malformed one fails the boot with the file name and line (and column, for TOML).

Environment variables

Variables that start with APP_ map onto the tree. Navree strips the prefix, splits the rest on double underscores and lower-cases each part:

VariableKey
APP_DATABASE__URLdatabase.url
APP_HTTP__CORS__ALLOWED_ORIGINShttp.cors.allowed_origins
APP_LOG__LEVELS__NAVREE_QUEUElog.levels.navree_queue
APP_APP__URLapp.url
APP_KEYkey

A single underscore stays part of the name: APP_HTTP__PER_IP_RATE_LIMIT is http.per_ip_rate_limit. The prefix is matched case-insensitively, as Windows treats variable names. A variable with an empty part, such as APP_HTTP____ADDR, is ignored.

Environment values are strings. Typed sections convert them when they read the tree, so APP_DATABASE__MAX_CONNECTIONS=20 fills a u32, and true, yes, on and 1 fill a bool. Lists can't be written as environment variables directly, so the settings that take one also accept a comma-separated string, for example APP_HTTP__TRUSTED_PROXIES=127.0.0.1,::1. Each module's page says which of its settings do.

The .env file

.env holds the settings of one machine: secrets and local overrides. navree new writes it as a copy of .env.example, and .gitignore excludes it. Commit .env.example instead, with the keys and no secrets.

.env

Entries map exactly like environment variables, and entries without the APP_ prefix are ignored. Navree reads .env with its own parser and never copies its entries into the process environment, so std::env::var doesn't see them.

The format:

  • KEY=value, one per line. Blank lines and lines starting with # are skipped, and an export prefix is allowed.
  • Unquoted values are trimmed, and a # after whitespace starts a comment. Backslashes are literal, so C:\Users stays as it is.
  • "Double-quoted" values may span lines and understand \n, \r, \t, \", \\ and \$.
  • 'Single-quoted' values may span lines and are taken literally.
  • ${NAME} in unquoted and double-quoted values expands to an earlier entry of the same file, or to nothing. It never reads the process environment.

A syntax error names the file and the line, never the value.

The config/app.toml file

config/app.toml holds the settings every environment shares. Its tables are the sections:

config/app.toml

Keys keep the case you write them in. Everything a module doesn't find falls back to its default, so the file only needs the settings you change. Keep secrets out of it: it is committed.

The application key

The application key is the app's master secret. It is 32 random bytes written as base64:..., and key:generate prints a new one:

Shell
Text

Set it as APP_KEY in .env or in the environment; Navree reads app.key, falling back to the top-level key that APP_KEY maps to. Several modules depend on it:

  • CryptoModule's encrypter uses it for AES-256-GCM encryption, for example of two-factor secrets.
  • AuthModule derives the JWT signing key from it, unless you set auth.jwt.secret.
  • HttpModule derives the key for signed URLs from it, unless you set http.signing_key.
  • RealtimeModule derives the key that signs broadcasts relayed between processes, unless you set realtime.secret.

Without a key the app still boots, so that key:generate can run, and logs a warning. Encryption then fails, and JWTs and signed URLs use a random key per process: every restart signs everyone out and breaks every signed link. Every process of an app (the server, queue workers, the scheduler) needs the same key.

A malformed key fails the boot, naming app.key.

Rotating the key

To replace a key without losing encrypted data, move the old key to app.previous_keys (APP_PREVIOUS_KEYS, a comma-separated list, most recent first) and set the new one. Previous keys only decrypt; everything new is encrypted with the current key.

.env

Previous keys don't apply to the keys derived for JWTs, signed URLs and relayed broadcasts: rotating APP_KEY signs every user out and invalidates outstanding signed links, unless those features have their own secrets. Encryption and hashing has the details.

Typed configuration sections

A module reads its settings as a struct that implements ConfigSection. The struct deserializes with serde, and KEY is its dotted path in the tree. Here is a billing section for a module of your own:

src/modules/billing/config.rs

Register the section in the module with m.config::<T>(). Its providers can then inject Arc<BillingConfig>:

src/modules/billing/mod.rs

m.config::<T>() registers T as a singleton of this module (not exported), built when the app boots:

  • A missing section deserializes from an empty table, so #[serde(default)] fields take their defaults. Without #[serde(default)], a missing required field fails the boot.
  • A value of the wrong type fails the boot, naming the section. The message never quotes the value, since it may be a secret.
  • validate runs next, and an error stops the boot too, with a message like this one:
Text

Wrap secrets in Secret<T>. It deserializes like T, prints [redacted] in Debug and Display, and has no Serialize impl, so it can't leak into a response or a log line. Read the value with .expose().

Reading values directly

Any provider can inject the whole tree as Arc<Config>. This is handy for a single value that several modules share, such as app.url:

Rust
MethodReturns
get_string(key)The scalar at a dotted path as a string (numbers and booleans formatted), or None.
get_bool(key)true/false, yes/no, on/off, 1/0 (any case) or a number, as a bool.
section::<T>(key)The subtree deserialized as T; a MissingSection error when it is absent.
section_or_default::<T>(key)The same, or T::default() when absent.
typed::<T>()A ConfigSection, read and validated exactly as m.config::<T>() does.

Custom sources

ConfigSources is a builder, so an app can read other files or another prefix. Sources apply in the order you add them:

MethodAdds
.dotenv(path)A .env-format file, mapped with the APP_ prefix.
.file(path)A TOML file.
.env()The process's APP_* variables, read at boot.
.env_prefixed("MYAPP")Variables with another prefix (MYAPP_ and MYAPP are the same).
.env_vars(prefix, pairs)A fixed list of variables, for tests.
.set(key, value)One value, applied above every source whatever the call order. An object merges into the existing table.

The benchmark's app, for example, maps the harness's own variables on top of the defaults:

src/main.rs

Environments

Navree has no environment switch: nothing reads an APP_ENV variable or an app.env key, and there are no per-environment files by default. Each machine's settings come from its own .env file and environment variables.

Two defaults depend on how the binary was built:

SettingDebug build (cargo run, navree dev)Release build (cargo build --release)
log.formatpretty, coloured on a terminaljson, one object per line
view.reloadon: templates reload when they changeoff: templates are read once

If you want a file per environment, add the source yourself. This reads config/<APP_ENV>.toml after config/app.toml, so its keys win:

src/main.rs

A missing config/<env>.toml is skipped like any missing file. APP_ENV here is read from the process environment, not from .env, and it also lands in the tree as the top-level key env, which nothing uses.

Keeping secrets out of logs

Config's Debug output, and Config::redacted(), print the tree with secrets masked. A value is replaced by [redacted] when its key contains password, secret, token, key, authorization or cookie (in any case), and the password of any scheme://user:password@host URL is masked. Other values are shown as they are, so give secrets secret-looking names. Configuration errors never include values.

The framework's sections

Each module documents its section on its own page:

SectionModulePage
app (key, previous_keys, url)severalEncryption and hashing
httpHttpModuleRouting, Rate limiting, CORS and security headers
logLoggerModuleLogging
hashingCryptoModuleEncryption and hashing
validationValidationModuleValidation
databaseDatabaseModuleDatabase: getting started
authAuthModuleAuthentication
auditAuditModuleThe audit log
cacheCacheModuleCache
queueQueueModuleQueues
mailMailModuleMail
notifyNotifyModuleNotifications
storageStorageModuleFile storage
http_clientHttpClientModuleHTTP client
realtimeRealtimeModuleReal-time broadcasting
viewViewModuleViews and templates
apiApiModuleOpenAPI
observeObserveModuleHealth and metrics
sessionSessionModuleSessions and CSRF

Configuration in tests

TestApp::builder() starts with no sources at all, so tests never read .env, config/app.toml or your environment. Add what a test needs with .config(key, value), or pass a whole ConfigSources with .sources(...); .config values stay on top:

Rust

To boot a whole app the way main does, build the sources with ConfigSources::new() and .set(...) rather than ConfigSources::defaults(), which reads the real APP_* variables of the machine running the tests. Your first app shows the pattern, and Testing covers the rest.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks