Autumn ships with sensible defaults for everything — config loading, the database pool, the session store, the telemetry subscriber, error pages, the request cache, route handlers, middleware. None of those defaults are written in stone. The framework gives you three different mechanisms to swap them out, each suited to a different scope of change. Knowing which tier a piece of behaviour lives in is the fastest way to find the right hook.

This guide names the three tiers, shows which subsystems live where, and points you at the per-tier how-tos.


The three tiers at a glance

TierMechanismScopeTypical use case
1AppBuilder::with_<subsystem>(impl Trait)Boot-time, one-per-appReplace a framework subsystem (config loader, DB pool, session store, telemetry, error pages)
2#[intercept(Layer::new(...))]Per-request, stackableAdd cross-cutting middleware (caching, custom auth, request shaping, tracing)
3Plugin::build(app)Distribution wrapper around tier 1 + 2Ship a reusable integration as a crate (e.g. autumn-aws-secrets-plugin)

These compose: a tier-3 Plugin typically does its work by calling tier-1 or tier-2 hooks inside build(). There is no fourth tier — if you find yourself reaching for one, file an issue.


Tier 1: boot-time subsystem replacement

Tier-1 hooks let you replace a framework subsystem with your own trait-implementing struct. Each is a fluent with_<subsystem> method on AppBuilder. They run exactly once during AppBuilder::run(), before the HTTP server starts.

Rust,no Run
use autumn_web::prelude::*;

#[autumn_web::main]
async fn main() {
    autumn_web::app()
        .with_config_loader(MyJsonConfigLoader::new("config.json"))
        .with_telemetry_provider(DatadogTelemetryProvider)
        .with_session_store(MyEncryptedCookieStore)
        .routes(routes![/* ... */])
        .run()
        .await;
}

Subsystems available at tier 1

SubsystemTraitBuilder methodDefault
Config loadingConfigLoaderwith_config_loaderTomlEnvConfigLoader (five-layer TOML + env)
Database poolDatabasePoolProviderwith_pool_providerDieselDeadpoolPoolProvider (deadpool + diesel-async)
TelemetryTelemetryProviderwith_telemetry_providerTracingOtlpTelemetryProvider (tracing-subscriber + optional OTLP)
Session storeSessionStorewith_session_storeMemoryStore or RedisStore based on session.backend config
Error pagesErrorPageRenderererror_pagesBuilt-in HTML renderer

See custom-subsystems.md for a per-subsystem how-to with full code examples.

When tier 1 is the right answer

  • You want one subsystem behaving differently for the whole app.
  • The replacement happens at boot time, not per-request.
  • You want type-checked integration with the rest of the framework.

Tier 2: per-request middleware via #[intercept]

Tier-2 hooks attach a tower-style Layer to a route or group of routes. They run once per matching request and can stack arbitrarily.

Rust,no Run
# use autumn_web::prelude::*;
# struct CacheResponseLayer;
# impl CacheResponseLayer { fn new(_: ()) -> Self { Self } }
# let cache = ();

#[get("/expensive")]
#[intercept(CacheResponseLayer::new(cache.clone()))]
async fn expensive() -> &'static str {
    "computed once, served many"
}

When tier 2 is the right answer

  • You're augmenting request handling, not boot-time behaviour.
  • You want the change to apply to a subset of routes (or all of them, but via stacking rather than replacement).
  • Multiple instances should be layered, not "last one wins".

Tier-2 examples

  • Caching#[intercept(CacheResponseLayer::new(my_cache))] is the intentional path for response caching. The Cache trait isn't a tier-1 install because there is no single "framework cache" — different routes benefit from different cache configurations.
  • Custom tracing — wrap a route with #[intercept(TracingLayer)] to emit additional spans without touching the global subscriber.
  • Auth shape variants — apply #[intercept(BearerAuthLayer)] to API routes while leaving session-cookie routes untouched.

Tier 3: distribution as a Plugin

Tier-3 packages tier-1 and tier-2 calls into a reusable struct that anyone can install with a single line. This is the right shape for cross-organisation distribution — publish a crate with a Plugin in it, users cargo add it and write .plugin(YourPlugin::new(...)).

Rust,no Run
use autumn_web::app::AppBuilder;
use autumn_web::plugin::Plugin;

pub struct AwsSecretsConfigPlugin {
    region: String,
}

impl Plugin for AwsSecretsConfigPlugin {
    fn build(self, app: AppBuilder) -> AppBuilder {
        // Tier-1 install inside the plugin's build() — the user just sees
        // `.plugin(AwsSecretsConfigPlugin::new("us-east-1"))`.
        app.with_config_loader(AwsSecretsConfigLoader::new(self.region))
    }
}

# pub struct AwsSecretsConfigLoader;
# impl AwsSecretsConfigLoader { fn new(_: String) -> Self { Self } }
# impl autumn_web::config::ConfigLoader for AwsSecretsConfigLoader {
#     async fn load(&self) -> Result<autumn_web::config::AutumnConfig, autumn_web::config::ConfigError> { unimplemented!() }
# }

End user:

Rust,no Run
# use autumn_web::prelude::*;
# struct AwsSecretsConfigPlugin;
# impl AwsSecretsConfigPlugin { fn new(_: &str) -> Self { Self } }
# impl autumn_web::plugin::Plugin for AwsSecretsConfigPlugin {
#     fn build(self, app: autumn_web::app::AppBuilder) -> autumn_web::app::AppBuilder { app }
# }
#[autumn_web::main]
async fn main() {
    autumn_web::app()
        .plugin(AwsSecretsConfigPlugin::new("us-east-1"))
        .run()
        .await;
}

When tier 3 is the right answer

  • You're packaging behaviour for someone else to consume.
  • The behaviour spans multiple tier-1 or tier-2 installs and should travel together as a unit (e.g. an OTel plugin that installs both a TelemetryProvider AND a per-request tracing layer).
  • You want conflict detection — two plugins claiming the same name() trigger a warning, so duplicate .plugin(...) calls don't silently shadow each other.

See autumn/src/plugin.rs for the trait definition and naming conventions for first-party vs third-party plugin crates.


Choosing between tiers — a quick decision tree

  1. "I want to change framework behaviour for the whole app, once at boot." → Tier 1.
  2. "I want to apply a wrapper to specific requests." → Tier 2.
  3. "I'm building this for someone else to install with one line." → Tier 3, wrapping tier-1 or tier-2 calls.

If a subsystem you want to replace doesn't have a tier-1 method yet, file an issue — adding one is mechanical and we generally welcome the patch.


Further reading