Settings and environment variables
How umbral reads umbral.toml, .env, and UMBRAL_-prefixed env vars, plus how to access settings from handlers, plugins, or task bodies.
Every umbral app has one ambient Settings value loaded once at boot from configuration files and environment variables, with later sources overriding earlier ones:
- Built-in defaults:
database_url = "sqlite::memory:",bind_addr = "127.0.0.1:8000", etc. umbral.tomlat the project root..envat the project root, merged like environment variables for local development.UMBRAL_-prefixed environment variables already set in the process.
After App::build runs, umbral::settings::get() returns the resolved value from anywhere in the app: handlers, plugin code, task bodies, signal handlers. No state plumbing, no extractors.
The known fields
pub struct Settings { pub database_url: String, // "sqlite::memory:" by default pub databases: HashMap<String, String>, // named secondary pools (optional) pub db_max_connections: u32, // 10 (pool size) pub db_acquire_timeout_secs: u64, // 30 (wait for a free connection) pub db_min_connections: u32, // 0 (idle-connection floor) pub db_idle_timeout_secs: Option<u64>, // Some(600); 0/empty disables pub db_max_lifetime_secs: Option<u64>, // Some(1800); 0/empty disables pub db_test_before_acquire: bool, // true (health-check on acquire) pub secret_key: String, // insecure dev default; required for prod pub environment: Environment, // Dev | Test | Prod (default Dev) pub allowed_hosts: Vec<String>, // ["localhost", "127.0.0.1"] by default pub log_level: String, // "info" pub bind_addr: String, // "127.0.0.1:8000" pub time_zone: Option<String>, // None (UTC everywhere) by default pub static_url: String, // "/static/" pub static_root: String, // "staticfiles/" pub extra: HashMap<String, toml::Value>, // catch-all for app-defined keys}| Field | Env var | umbral.toml key | Default |
|---|---|---|---|
database_url | UMBRAL_DATABASE_URL | database_url | "sqlite::memory:" |
databases | UMBRAL_DATABASES__<NAME> (double underscore) | [databases] table | empty map |
db_max_connections | UMBRAL_DB_MAX_CONNECTIONS | db_max_connections | 10 |
db_acquire_timeout_secs | UMBRAL_DB_ACQUIRE_TIMEOUT_SECS | db_acquire_timeout_secs | 30 |
db_min_connections | UMBRAL_DB_MIN_CONNECTIONS | db_min_connections | 0 |
db_idle_timeout_secs | UMBRAL_DB_IDLE_TIMEOUT_SECS | db_idle_timeout_secs | Some(600) (0/empty disables) |
db_max_lifetime_secs | UMBRAL_DB_MAX_LIFETIME_SECS | db_max_lifetime_secs | Some(1800) (0/empty disables) |
db_test_before_acquire | UMBRAL_DB_TEST_BEFORE_ACQUIRE | db_test_before_acquire | true |
secret_key | UMBRAL_SECRET_KEY | secret_key | "umbral-insecure-dev-key-change-me" (rejected in prod) |
environment | UMBRAL_ENVIRONMENT | environment | Dev (values: Dev / Test / Prod) |
allowed_hosts | UMBRAL_ALLOWED_HOSTS (comma-list) | allowed_hosts | ["localhost", "127.0.0.1"] |
log_level | UMBRAL_LOG_LEVEL | log_level | "info" |
bind_addr | UMBRAL_BIND_ADDR | bind_addr | "127.0.0.1:8000" |
time_zone | UMBRAL_TIME_ZONE | time_zone | None (treats naive datetimes as UTC) |
static_url | UMBRAL_STATIC_URL | static_url | "/static/" |
static_root | UMBRAL_STATIC_ROOT | static_root | "staticfiles/" |
umbral.toml
The project's committed configuration file. Lives at the project root next to Cargo.toml:
# umbral.tomldatabase_url = "sqlite://app.db?mode=rwc"bind_addr = "0.0.0.0:8000"secret_key = "replace-me-with-openssl-rand-hex-32"environment = "Prod"log_level = "info"allowed_hosts = ["example.com", "www.example.com"] # Named secondary pools, routed per-model via `#[umbral(database = "alias")]`# or per-plugin via `Plugin::database()`[databases]analytics = "postgres://analytics:pw@db-host/analytics"cache = "sqlite://cache.db?mode=rwc"Commit umbral.toml to git for everything that isn't a secret. Keep secrets (secret_key, real database_url with passwords) out of the committed copy and supply them via env vars instead.
Environment variables (production / containers)
export UMBRAL_DATABASE_URL="postgres://app:pw@db/app"export UMBRAL_BIND_ADDR="0.0.0.0:8080"export UMBRAL_SECRET_KEY="$(openssl rand -hex 32)"export UMBRAL_ENVIRONMENT="Prod" cargo run -- serveThe UMBRAL_ prefix marks the env var as framework-owned. Anything else in your shell is invisible to umbral's settings loader, so there's no accidental capture.
Reading settings from anywhere
// In a handler:async fn home() -> Html<String> { let settings = umbral::settings::get(); Html(format!("running in {:?} mode", settings.environment))} // In a Plugin::on_ready:fn on_ready(&self, _ctx: &AppContext) -> Result<(), PluginError> { let settings = umbral::settings::get(); if matches!(settings.environment, umbral::Environment::Prod) { // production-only setup } Ok(())} // In a #[task] body:#[task]async fn ping_external_api(payload: PingPayload) -> Result<(), String> { let api_key = umbral::settings::get_opt() .and_then(|s| s.extra_str("openai_api_key").map(str::to_string)) .ok_or("OPENAI_API_KEY not configured")?; // ... call the API Ok(())}umbral::settings::get() returns &'static Settings and panics if App::build hasn't run. umbral::settings::get_opt() returns Option<&'static Settings>; use it in code that may run before App::build finishes (rare, but possible in macro-emitted helpers, test fixtures, etc.).
Custom settings (the extra field)
Most real apps need keys umbral doesn't know about: OPENAI_API_KEY, STRIPE_SECRET, third-party plugin config. The extra: HashMap<String, toml::Value> field catches everything that doesn't match a known field name:
# Env varexport UMBRAL_OPENAI_API_KEY="sk-test"# umbral.toml - both workopenai_api_key = "sk-test" # Or nested[external.openai]api_key = "sk-test"model = "gpt-4o"Read with extra_str for the common scalar-string case:
let key = umbral::settings::get() .extra_str("openai_api_key") .ok_or("missing OPENAI_API_KEY")?;For nested values ([external.openai]), index extra directly:
let cfg = umbral::settings::get();let model: &str = cfg.extra .get("external") .and_then(|v| v.get("openai")) .and_then(|v| v.get("model")) .and_then(|v| v.as_str()) .unwrap_or("gpt-4o");.env files
Settings::from_env() automatically reads a project-root .env file before reading UMBRAL_ variables from the process environment. This is the local-dev place for values you do not want to commit:
cat > .env <<'EOF'UMBRAL_DATABASE_URL=postgres://app:pw@localhost/app_devUMBRAL_SECRET_KEY=dev-only-secretEOF cargo run -- serveValues already exported by the shell, Docker, systemd, or a hosting provider win over .env. That keeps production configuration explicit while still making local development convenient.
Precedence summary
Later wins:
- Struct defaults (in
Settings::default_*functions). umbral.tomlat the project root.UMBRAL_variables from.env, unless the process already set the same key.UMBRAL_-prefixed env vars already present in the process environment.- The
--addrflag onumbral serve(one-off override; doesn't write back to settings).
When the server starts it logs the bound address:
INFO umbral_core::app: umbral serving on 127.0.0.1:8000Loading settings yourself (rare)
App::builder().settings(...) accepts a pre-built Settings. The common shape uses from_env:
let settings = umbral::Settings::from_env()?; // figment-loadedlet app = App::builder().settings(settings).build()?;For tests, build a Settings literal directly:
let settings = umbral::Settings { database_url: "sqlite::memory:".into(), environment: umbral::Environment::Test, ..umbral::Settings::from_env()?};See also
- Management commands: env vars affect every subcommand the same way.
- Connection pooling: the
db_*knobs above (db_max_connections,db_idle_timeout_secs, …) in depth. - Backends, Postgres and SQLite: what to put in
database_urlper backend. umbral-tasks:settings::get()works in task handlers too.umbral-signals: same in signal handlers.