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

The plugin contract: batteries as real plugins

Dependencies point inward toward the core; control flows outward through a trait. That single rule is what lets you keep the official auth, swap it for your SSO, or build a project-specific plugin — all behind the same boundary.

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 umbral facade, never the reverse.
  • umbral-core defines the Plugin trait but never names a concrete plugin — it touches plugins only as Box<dyn Plugin>.
  • umbral-core depends on neither umbral-rest nor umbral-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 of AppConfig.ready())

Wired in one line

Adding a plugin to your app is a single builder call — no registry edits, no glue module to maintain:

App::builder()
    .plugin(AuthPlugin::<AuthUser>::default())
    .plugin(SessionsPlugin::default())
    .plugin(AdminPlugin::default())
    .plugin(RestPlugin::default())
    .plugin(MyOwnPlugin::default())
    .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.