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

The Plugin trait

umbral's only extension mechanism. Built-ins and third-party plugins are structurally identical.

Every umbral extension is a Plugin. Auth, sessions, admin, tasks, and REST are plugins. So is every third-party crate that ships models, routes, or commands. The trait is the architectural keystone: if a built-in needed special-casing to fit, the contract would be wrong.

The trait

Code
rust
use umbral::prelude::*;
 
pub struct BlogPlugin;
 
impl Plugin for BlogPlugin {
fn name(&self) -> &'static str { "blog" }
 
fn dependencies(&self) -> &'static [&'static str] { &["auth"] }
 
fn models(&self) -> Vec<umbral::migrate::ModelMeta> {
vec![umbral::migrate::ModelMeta::for_::<Post>()]
}
 
fn routes(&self) -> Router {
Router::new().route("/posts", get(list_posts))
}
 
// system_checks and on_ready default to no-op.
}

Register it on the builder:

Code
rust
App::builder()
.settings(settings)
.database("default", pool)
.plugin(BlogPlugin)
.build()?;

App::build() topologically sorts the registered plugins by their dependencies(), runs each plugin's system_checks() alongside the framework's, merges every plugin's routes() into the app router, and fires on_ready in dependency order.

What plugins contribute

Every method except name() has a default that returns the empty contribution, so a plugin opts in only to what it ships: a pure-route plugin overrides routes(), a pure-data plugin overrides models(), and the auth plugin overrides almost all of them.

name (required)

&'static str. Unique within the binary; the migration tracking key. The reserved name 'app' belongs to the implicit plugin that owns .model::<T>() registrations.

dependencies

&'static [&'static str]. Plugins that must load first. The topological sort uses this; cycles surface as BuildError::PluginCycle.

models

Vec<ModelMeta>. Per-plugin model list, in declaration order. The migration engine diffs these for makemigrations.

routes

An axum Router. Plugins choose their own path prefixes; the App merges them all flat.

system_checks

Vec<SystemCheck>. Boot-time invariants run in phase 4. Severity::Error blocks boot; Severity::Warning logs and continues.

on_ready

Sync hook fired after system checks pass, in dependency order. Spawn background work, wire signals, seal late registrations.

The full method surface

Every method below is a real trait method with a default impl (only name() is required). Signatures match crates/umbral-core/src/plugin.rs exactly.

MethodSignatureDefaultWhat it contributes
namefn name(&self) -> &'static str- (required)Stable identifier; migration-tracking key.
dependenciesfn dependencies(&self) -> &'static [&'static str]&[]Plugins that must load first.
modelsfn models(&self) -> Vec<ModelMeta>emptyThe plugin's models, the migration diff target.
routesfn routes(&self) -> RouterRouter::new()HTTP routes, merged flat into the app router.
route_pathsfn route_paths(&self) -> Vec<RouteSpec>emptyDeclared paths for the dev-mode 404 page (informational only).
openapi_pathsfn openapi_paths(&self) -> Vec<(String, serde_json::Value)>emptyOpenAPI path items umbral-openapi merges into the spec.
system_checksfn system_checks(&self) -> Vec<SystemCheck>emptyBoot-time invariants (phase 4).
provides_storagefn provides_storage(&self) -> boolfalsetrue if on_ready registers a Storage backend (e.g. StoragePlugin).
databasefn database(&self) -> Option<&'static str>NoneDB alias every model this plugin owns reads/writes, for multi-database routing.
templates_dirsfn templates_dirs(&self) -> Vec<PathBuf>emptyTemplate search directories.
template_registrarsfn template_registrars(&self) -> Vec<TemplateRegistrar>emptyCustom minijinja filters / functions / globals the plugin makes available in templates.
wrap_routerfn wrap_router(&self, router: Router) -> Routerrouter unchangedThe tower/axum middleware seam (see below).
middlewarefn middleware(&self) -> Vec<Arc<dyn Middleware>>emptyErgonomic before_request / after_response hooks, stacked in onion order.
static_filesfn static_files(&self) -> Vec<StaticFile>emptyAssets baked into the binary (&'static [u8] bodies).
static_dirsfn static_dirs(&self) -> Vec<StaticDir>emptyOn-disk source dirs served live (dev) under a namespace via the unified static pipeline.
static_root_dirsfn static_root_dirs(&self) -> Vec<PathBuf>emptyOn-disk dirs served at the bare root of static_url (no namespace).
commandsfn commands(&self) -> Vec<Box<dyn PluginCommand>>emptyCLI subcommands.
api_endpointsfn api_endpoints(&self) -> Vec<ApiEndpoint>emptyEndpoints advertised for service discovery (a REST API root, a SPA).
on_readyfn on_ready(&self, ctx: &AppContext) -> Result<(), PluginError>Ok(())Late-init hook.

Notable hooks in depth

  • wrap_router(Router) -> Router - the middleware seam. Each plugin wraps the merged router with its own tower / axum layers; App::build calls it in topological order, so a plugin's layers wrap everything declared before it. umbral-security (CSRF + hardening headers) and umbral-sessions (the session layer) both ride this hook. Returning the router shape - rather than a Vec<Layer> - sidesteps the trait-object lifetime problem Layer's generics produce, so plugins keep the full axum/tower API at the call site.
  • middleware() -> Vec<Arc<dyn Middleware>> - the ergonomic alternative to wrap_router. Each Middleware gets a before_request / after_response hook and nothing else to wire; all plugins' (plus the app's) are collected into one stack installed at App::build in onion order. Reach for wrap_router when you need a real tower Layer (timeouts, tracing spans, body-limit); reach for middleware when you just want to look at the request or response.
  • commands() -> Vec<Box<dyn PluginCommand>> - CLI subcommands. umbral-auth ships createsuperuser and umbral-tasks ships tasks-worker through this hook. See Management commands.
  • templates_dirs() / template_registrars() - plugins contribute template directories and custom tags. See Rendering HTML.
  • static_files() - embedded assets baked into the binary; each entry becomes one GET <url_path> route serving a &'static [u8] body (typically include_bytes!). umbral-admin ships its precompiled CSS this way. static_dirs() / static_root_dirs() instead serve assets off disk through the unified static pipeline (live in dev); umbral-playground ships its Vite bundle via static_dirs().

on_ready receives an AppContext carrying the default DbPool and a Settings clone. The hook is sync (the trait must be object-safe for Vec<Box<dyn Plugin>>); bridge to async work with umbral::plugin::block_on_ready(fut), which is safe under multi-thread, current-thread (#[tokio::test]), and no-runtime callers.

What's deferred

  • A declarative middleware() whose entries are ordered tower Layers. The ergonomic before_request / after_response form is shipped - including declarative ordering via Middleware::order() -> i32 (lower = outer; the stack is stable-sorted before install, so a middleware places itself regardless of registration timing). What's still deferred is a declarative form over raw tower Layers - cross-plugin Layer ordering runs into the trait-object lifetime problem Layer's generics produce; use wrap_router when you need a raw Layer.
  • inventory / linkme auto-registration is intentionally not pursued - implicit compile-time collection contradicts umbral's explicit App::builder().plugin(...) / .middleware(...) wiring (the framework keeps exactly one intentional global). Middleware is always contributed explicitly.

The on_ready context

on_ready receives an &AppContext:

Code
rust
pub struct AppContext {
pub pool: DbPool, // the default connection pool, typed by backend
pub settings: Settings, // a clone of the active settings
}

pool is the same value umbral::db::pool_dispatched().clone() returns. Plugin code that needs the database normally goes through the ORM (Model::objects()…); this field is the escape hatch for schema-DDL bootstrap and backend-specific features like Postgres RLS.

The proof

plugins/umbral-auth/ is the first built-in plugin and uses exactly the contract above with zero special-casing inside umbral-core. See Users and passwords for the shape.

The spec lives in docs/specs/02-plugin-contract.md.