This version is in beta. Some features may change before release.

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:

  1. Built-in defaults: database_url = "sqlite::memory:", bind_addr = "127.0.0.1:8000", etc.
  2. umbral.toml at the project root.
  3. .env at the project root, merged like environment variables for local development.
  4. 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

Code
rust
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
}
FieldEnv varumbral.toml keyDefault
database_urlUMBRAL_DATABASE_URLdatabase_url"sqlite::memory:"
databasesUMBRAL_DATABASES__<NAME> (double underscore)[databases] tableempty map
db_max_connectionsUMBRAL_DB_MAX_CONNECTIONSdb_max_connections10
db_acquire_timeout_secsUMBRAL_DB_ACQUIRE_TIMEOUT_SECSdb_acquire_timeout_secs30
db_min_connectionsUMBRAL_DB_MIN_CONNECTIONSdb_min_connections0
db_idle_timeout_secsUMBRAL_DB_IDLE_TIMEOUT_SECSdb_idle_timeout_secsSome(600) (0/empty disables)
db_max_lifetime_secsUMBRAL_DB_MAX_LIFETIME_SECSdb_max_lifetime_secsSome(1800) (0/empty disables)
db_test_before_acquireUMBRAL_DB_TEST_BEFORE_ACQUIREdb_test_before_acquiretrue
secret_keyUMBRAL_SECRET_KEYsecret_key"umbral-insecure-dev-key-change-me" (rejected in prod)
environmentUMBRAL_ENVIRONMENTenvironmentDev (values: Dev / Test / Prod)
allowed_hostsUMBRAL_ALLOWED_HOSTS (comma-list)allowed_hosts["localhost", "127.0.0.1"]
log_levelUMBRAL_LOG_LEVELlog_level"info"
bind_addrUMBRAL_BIND_ADDRbind_addr"127.0.0.1:8000"
time_zoneUMBRAL_TIME_ZONEtime_zoneNone (treats naive datetimes as UTC)
static_urlUMBRAL_STATIC_URLstatic_url"/static/"
static_rootUMBRAL_STATIC_ROOTstatic_root"staticfiles/"

umbral.toml

The project's committed configuration file. Lives at the project root next to Cargo.toml:

Code
toml
# umbral.toml
database_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)

Code
bash
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 -- serve

The 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

Code
rust
// 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.).

Info
The `&'static` lifetime is real: the settings live for the process. You can stash field clones (`settings.secret_key.clone()`) freely; the underlying struct doesn't move.

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:

Code
bash
# Env var
export UMBRAL_OPENAI_API_KEY="sk-test"
Code
toml
# umbral.toml - both work
openai_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:

Code
rust
let key = umbral::settings::get()
.extra_str("openai_api_key")
.ok_or("missing OPENAI_API_KEY")?;

For nested values ([external.openai]), index extra directly:

Code
rust
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:

Code
bash
cat > .env <<'EOF'
UMBRAL_DATABASE_URL=postgres://app:pw@localhost/app_dev
UMBRAL_SECRET_KEY=dev-only-secret
EOF
 
cargo run -- serve

Values 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:

  1. Struct defaults (in Settings::default_* functions).
  2. umbral.toml at the project root.
  3. UMBRAL_ variables from .env, unless the process already set the same key.
  4. UMBRAL_-prefixed env vars already present in the process environment.
  5. The --addr flag on umbral serve (one-off override; doesn't write back to settings).

When the server starts it logs the bound address:

Code
txt
INFO umbral_core::app: umbral serving on 127.0.0.1:8000

Loading settings yourself (rare)

App::builder().settings(...) accepts a pre-built Settings. The common shape uses from_env:

Code
rust
let settings = umbral::Settings::from_env()?; // figment-loaded
let app = App::builder().settings(settings).build()?;

For tests, build a Settings literal directly:

Code
rust
let settings = umbral::Settings {
database_url: "sqlite::memory:".into(),
environment: umbral::Environment::Test,
..umbral::Settings::from_env()?
};

See also

getting-startedsettingsenv