Skip to content

Configuration

Oxide reads .env on boot and loads every config/*.toml file into a merged tree keyed by filename stem. Values support shell-style interpolation: "${APP_NAME:-My App}".

Files

A new project ships with:

config/
├── app.toml
└── database.toml

Add config/whatever.toml and it’s reachable at the whatever.* key path with zero extra wiring.

Reading config

Three flavours, pick whichever fits the call site:

use oxide::http::prelude::*;
// 1. Free helper — works anywhere.
let name: String = config("app.name").unwrap_or_default();
// 2. Off the request — same semantics, scoped per-request.
async fn handler(req: Request) -> Response {
let debug: bool = req.config("app.debug").unwrap_or(false);
// ...
Response::text("ok")
}
// 3. Typed section, resolved via the container — bound at boot.
#[derive(serde::Deserialize)]
struct AppConfig {
name: String,
debug: bool,
}
// In ConfigServiceProvider::register:
// app.bind_config::<AppConfig>("app");
// In a handler:
let typed = req.resolve::<AppConfig>();
println!("{}", typed.name);

Environment overrides

Inside a TOML value, wrap the env var in ${...}:

name = "${APP_NAME:-Billing API}"
debug = "${APP_DEBUG:-false}"
  • ${VAR} — required; fails loud if unset.
  • ${VAR:-default} — falls back to default if unset.
  • When the whole value is one ${...} expression, it auto-coerces to bool / int / float where the target field expects it.

Disk vs embedded loading

Two ways to feed config/*.toml into the framework. Both run the same env-interpolation pass, so ${VAR:-default} overrides work identically.

Disk (default for dev)

src/providers/config_service_provider.rs
impl ServiceProvider for ConfigServiceProvider {
fn register(&self, app: &Application) {
app.load_config("config").expect("config/ directory missing");
}
}

Files are read from disk relative to the process’s cwd at startup. Edit a TOML file → restart → new values pick up. No rebuild needed.

Tradeoff: deployments must ship the binary AND the config/ directory together. Cwd has to be the project root (or wherever config/ lives).

Embedded (default for new scaffolds)

src/providers/config_service_provider.rs
use oxide::config::{include_dir, Dir};
const CONFIG: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/config");
impl ServiceProvider for ConfigServiceProvider {
fn register(&self, app: &Application) {
app.load_config_embedded(&CONFIG).expect("baked config invalid");
}
}

include_dir! reads the whole config/ tree at compile time and bakes the bytes into the binary. At startup, load_config_embedded parses the embedded TOML and runs the same env-interpolation — ${VAR:-default} overrides still apply.

Tradeoff: editing a TOML default forces a rebuild. The win is a single self-contained binary — scp ./billing-api host:/srv/, done. No config/ directory to ship, no cwd dependency.

.env is always read from disk at runtime regardless of which mode you pick — it’s the per-deploy override mechanism.

When to switch

  • New scaffolds default to embedded. Single-binary deploys are the common case.
  • Switch to disk while iterating heavily on TOML in dev — saves the rebuild round-trip.
  • Keep on embedded for production. Edits-need-rebuild is an acceptable cost; “binary + config dir must stay together” is a recurring footgun.

Tunable framework knobs

Env varDefaultWhat it controls
APP_LISTEN127.0.0.1:8000address the HTTP server binds
APP_BODY_MAX_MB10request body size cap
APP_REQUEST_TIMEOUT_SECS30per-request timeout
APP_SHUTDOWN_TIMEOUT_SECS30graceful drain window on SIGTERM
DB_CONNECTIONsqlitewhich [connections.*] section to use
BCRYPT_ROUNDS12password hash cost (matches Laravel’s default)

Set any of these in .env or your deployment’s environment.