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

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.

Code
bash
cargo install umbral-cli

This puts the global umbral binary on your PATH - the scaffolding tool for creating projects and plugins.

Create a project

Code
bash
umbral startproject my_project

Scaffolds 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

Code
bash
cd my_project
cargo run -- dev

dev 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).

Note

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.

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

Warning
The `sqlite::memory:` default doesn't persist between subcommand invocations, so `cargo run -- migrate` quietly targets a throwaway database. Export `UMBRAL_DATABASE_URL=sqlite://app.db?mode=rwc` for any real work.

What's next

The full builder shape lives in docs/specs/01-app-and-settings.md.