Umbral's strongest public argument is also its simplest internal rule: the batteries are real plugins. The official auth, sessions, admin, tasks, and REST implement the exact same Plugin trait a community developer would.
Dependency inversion is the whole game
Dependencies point inward toward core. Control flows outward through the trait.
- Every plugin depends on the
umbralfacade, never the reverse. umbral-coredefines thePlugintrait but never names a concrete plugin — it touches plugins only asBox<dyn Plugin>.umbral-coredepends on neitherumbral-restnorumbral-openapi. That's the structural proof that "serializers are a plugin." Cargo's ban on circular dependencies enforces it for us.
What a plugin can contribute
A plugin (an "app") can contribute any subset of:
- models (which become migrations)
- routes and views
- middleware
- management commands
- a typed settings schema with defaults
- admin registrations
- lifecycle hooks (
on_ready()is the Rust version ofAppConfig.ready())
Wired in one line
Adding a plugin to your app is a single builder call — no registry edits, no glue module to maintain:
builder
.plugin
.plugin
.plugin
.plugin
.plugin
.build?;
Swap a battery without a rewrite
Because the boundary is identical first-party or third, you can swap the default auth for your SSO, drop in a community GraphQL plugin, or build a project-specific plugin with startapp — and the admin, the ORM, and the rest of your app keep working unchanged.
# enable batteries — they're just plugins
umbral-admin = "0.1"
umbral-rest = "0.1"
acme-graphql = "0.3" # a community plugin, same contract
That's the entire pitch: a productive, batteries-included app framework where no capability is privileged, and the one you want to replace is always replaceable.