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.
Install & scaffold
The fastest way to start is the umbral command-line tool: install it once, scaffold a project, and run it.
Install the CLI
Install the umbral CLI with cargo.
cargo install umbral-cliThis puts the global umbral binary on your PATH - the scaffolding tool for creating projects and plugins.
Create a project
umbral startproject my_projectScaffolds a ready-to-run project in ./my_project - a src/ tree, a Cargo.toml wired to the published umbral-* crates, and starter config. (Add plugins later from inside the project with umbral startplugin NAME.)
Run it
cd my_projectcargo run -- devdev is the development loop: it watches src/ and your templates and re-runs on change (templates hot-reload without a restart). The console prints the address it's serving on (e.g. http://127.0.0.1:8000).
cargo run -- dev needs cargo-watch - install it with cargo install cargo-watch. Without it, run the server directly with cargo run and restart after code edits; templates still hot-reload in dev mode either way. Other management commands run the same way: cargo run -- migrate, cargo run -- makemigrations, cargo run -- serve.
Since you've got umbral-cli installed, umbral is a shorthand for cargo run -- when run inside a project - so umbral dev, umbral migrate, etc. work too. The explicit cargo run -- form always works, install or not.
The shape
The section below is the minimal app written by hand - the same builder the scaffold sets up for you.
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.