Your first app
Boot an umbral app with Settings, a SQLite pool, and one route.
The minimal umbral app boots through one builder, registers a database pool, and serves a hand-written route. From there you grow into models, migrations, and plugins.
The shape
use umbral::prelude::*; #[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let settings = umbral::Settings::from_env()?; let pool = umbral::db::connect(&settings.database_url).await?; let app = App::builder() .settings(settings) .database("default", pool) .routes(Routes::new().get("/", || async { "hello, umbral" })) .build()?; app.serve("127.0.0.1:8000".parse()?).await?; Ok(())}App::builder().build() runs five phases under the hood: collect, detect the backend, publish the ambient state, run system checks, and merge plugin routers. From the caller's side it's a single line.
Settings
Settings::from_env() layers defaults, an optional umbral.toml, and UMBRAL_-prefixed environment variables (last wins). The fields that matter on day one:
database_url
The DB connection string. Default sqlite::memory:, fine for tests but not for the management CLI. Override with UMBRAL_DATABASE_URL.
secret_key
Set via UMBRAL_SECRET_KEY. The dev default is rejected in production.
What's next
- Declare a model and let the derive generate the
Modelimpl. - Run a migration: declare, migrate, change, migrate.
- Relationships (ForeignKey): link models with
ForeignKey<T>and eager-load withselect_related. - Transactions: wrap multi-step writes in
umbral::transaction(...)so they commit or roll back together. - Querying: F-expressions, Q-objects, exclude, projections, mutate-side terminals, subqueries.
- Port an existing database.
The full builder shape lives in docs/specs/01-app-and-settings.md.