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.tomlAdd 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 todefaultif unset.- When the whole value is one
${...}expression, it auto-coerces tobool/int/floatwhere 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)
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)
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 var | Default | What it controls |
|---|---|---|
APP_LISTEN | 127.0.0.1:8000 | address the HTTP server binds |
APP_BODY_MAX_MB | 10 | request body size cap |
APP_REQUEST_TIMEOUT_SECS | 30 | per-request timeout |
APP_SHUTDOWN_TIMEOUT_SECS | 30 | graceful drain window on SIGTERM |
DB_CONNECTION | sqlite | which [connections.*] section to use |
BCRYPT_ROUNDS | 12 | password hash cost (matches Laravel’s default) |
Set any of these in .env or your deployment’s environment.