Umbral
Back to the blog
DesignNote Jul 9, 2026 7 min read

Why Umbral exists

Rust has fast routers and solid ORMs. What it didn't have was the batteries-included feeling: declare your data and get migrations, an admin, forms, and a REST API almost for free. Umbral is that feeling, rebuilt on Rust's guarantees.

Rust already has excellent web building blocks. axum is a great router, sqlx is a great database layer, serde is a great serializer. What it didn't have, until now, is what makes a framework productive: one where you declare your data once and get migrations, CRUD, an admin, forms, and an optional REST API almost for free.

Umbral ('of the shadow', from Latin umbra, shadow) is a deliberate attempt to bring that batteries-included feeling to Rust's compile-time guarantees.

The Umbral plugin directory, built with Umbral

The one idea that matters most

Thin core, plugin-heavy. The framework dogfoods its own plugin system. Auth, sessions, admin, tasks, and REST are all plugins. Structurally they are identical to a third-party one. A REST-free app compiles and runs with zero serializer code. If a built-in can't be expressed as a plugin, the plugin contract is wrong.

That single constraint shapes everything. There is no privileged "core" path that the built-ins get to use and you don't.

Declare a model, get a migration

The everyday loop works from the first milestone that has models:

#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, Model)]
#[umbral(plugin = "blog")]
pub struct Post {
    pub id: i64,
    #[umbral(unique, max_length = 160)]
    pub slug: String,
    pub title: String,
    #[umbral(widget = "markdown")]
    pub body: String,
    #[umbral(auto_now_add)]
    pub created_at: chrono::DateTime<chrono::Utc>,
}

Then the two commands that are the product:

cargo run -- makemigrations   # autodetect the diff, write a migration
cargo run -- migrate          # apply pending migrations

Change the model, run them again, and the autodetector emits the right ALTER/DROP. This declare → migrate → change → migrate cycle isn't a later feature — it's the north star.

The type system does the work

A nullable column becomes Option<T>. Errors are Result values that flow through ?. SQL is always parameterized. The easy path is the safe path, enforced by the compiler rather than by a linter or a code review checklist.

Secure by default

CSRF protection, clickjacking and HSTS headers, template auto-escaping, and always-parameterized SQL ship on by default. A field declares which backends it supports, and a boot-time system check fails loudly on an incompatible field rather than at 3am in production.

Umbral is pre-alpha, and honest about it. But the shape is settled, and the everyday loop already works. That's why this whole website is built with it.