Configuration

Programmatic Configuration — Builder

RuniqueApp::builder(config) returns a RuniqueAppBuilder. There is only one builder — there are no two separate versions.

Minimal example

let app = RuniqueApp::builder(config)
    .routes(url::routes())
    .with_database(db)
    .statics()
    .build()
    .await?;

app.run().await?;

Available methods

Database

// Option 1: direct connection (DatabaseConnection)
let db_config = DatabaseConfig::from_env()?.build();
let db = db_config.connect().await?;

let app = RuniqueApp::builder(config)
    .with_database(db)       // takes a DatabaseConnection
    .routes(router)
    .build()
    .await?;

// Option 2: deferred connection (DatabaseConfig)
let db_config = DatabaseConfig::from_env()?.build();

let app = RuniqueApp::builder(config)
    .with_database_config(db_config)  // connection happens at .build()
    .routes(router)
    .build()
    .await?;

Routes

use runique::{urlpatterns, view};

pub fn routes() -> Router {
    urlpatterns! {
        "/" => view!{ views::index }, name = "index",
        "/about" => view!{ views::about }, name = "about",
    }
}

let app = RuniqueApp::builder(config)
    .routes(routes())
    .build()
    .await?;

Error handling

let app = RuniqueApp::builder(config)
    .with_error_handler(true)   // Enable error handler (default: true)
    .routes(router)
    .build()
    .await?;

Middlewares

All middleware configuration goes through .middleware(|m| { ... }) where m is a MiddlewareStaging:

let app = RuniqueApp::builder(config)
    .routes(router)
    .middleware(|m| {
        m.with_csp(|c| c.with_header_security(true))                        // Enable Content Security Policy
         .with_allowed_hosts(|h| h.enabled(true).host("mydomain.com"))      // Enable host validation
         .with_cache(true)              // Enable no-cache in dev
         .with_debug_errors(true)       // Enable detailed errors
    })
    .build()
    .await?;

Host validation is enabled via .with_allowed_hosts(|h| h.enabled(true).host("...")) in the builder — without this call, validation is disabled. No .env variable controls this behaviour.

is_debug() — global helper available via use runique::prelude::*. Returns true if DEBUG=true in .env. Read once at startup (LazyLock), available everywhere without passing a parameter.

Session duration

use tower_sessions::cookie::time::Duration;

let app = RuniqueApp::builder(config)
    .with_session_duration(Duration::hours(2))  // Default: 24h
    .routes(router)
    .build()
    .await?;

Or via .middleware() for advanced options:

.middleware(|m| {
    m.with_session_duration(Duration::hours(2))
     .with_anonymous_session_duration(Duration::minutes(5))
     .with_session_memory_limit(128 * 1024 * 1024, 256 * 1024 * 1024)
})

Framework logs

RuniqueLog centralises all logging configuration: the global tracing subscriber level and the internal framework categories.

The subscriber is initialised automatically by build()no call to init_logging() is needed in main.rs.

Everything goes through .with_log(|l| ...) — the closure receives an empty RuniqueLog and returns the final configuration.

use tracing::Level;

// Fine-grained per-category control — each submodule takes its own closure
RuniqueApp::builder(config)
    .with_log(|l| l
        .middleware(|m| m.csrf(Level::WARN))
        .session(|s| s.store(Level::WARN))
        .db(|d| d.connect(Level::INFO))
    )
    .routes(router)
    .build()
    .await?;

subscriber_level — subscriber level

Default: "debug" if DEBUG=true in .env, otherwise "warn". The RUST_LOG environment variable always takes priority.

RuniqueApp::builder(config)
    .with_log(|l| l.subscriber_level("info"))
    .routes(router)
    .build()
    .await?;

.dev() — enable everything in development

Preset that activates all categories at DEBUG level. No-op if DEBUG is not true in .env — safe to use unconditionally.

// Dev only (no-op if DEBUG != true)
RuniqueApp::builder(config)
    .with_log(|l| l.dev())
    .routes(router)
    .build()
    .await?;

// Dev with subscriber level override
RuniqueApp::builder(config)
    .with_log(|l| l.dev().subscriber_level("info").db(|d| d.connect(Level::INFO)))
    .routes(router)
    .build()
    .await?;

Available categories

RuniqueLog only exposes root methods per submodule (forms, middleware, session, auth, admin, db, mailer, migration, templates, errors, builder), each taking a closure into its own nested builder — there is no flat .csrf()/.session() method directly on RuniqueLog:

Category (nested leaf)AccessWhat is logged
csrf.middleware(|m| m.csrf(...))CSRF token detected in a GET URL (silent cleanup)
exclusive_login.session(|s| s.exclusive_login(...))Sessions invalidated on exclusive login
filter_fn.admin(|a| a.filter_fn(...))Failed filter_fn in the admin list view
roles.admin(|a| a.roles(...))Errors accessing the admin roles registry
password_init.auth(|a| a.password_init(...))password_init() called more than once
store.session(|s| s.store(...))Memory watermarks, large records, cleanup errors
connect.db(|d| d.connect(...))DB connection in progress / connection established

Secondary database — with_custom_db

Attaches any Any + Send + Sync + 'static value into the RuniqueEngine. Useful for connecting MongoDB, Redis, or any other external source. Can be called multiple times with different types.

let mongo = mongodb::Client::with_uri_str("mongodb://localhost:27017").await?;
let redis = redis::Client::open("redis://localhost")?;

let app = RuniqueApp::builder(config)
    .with_custom_db(mongo)
    .with_custom_db(redis)
    .routes(url::routes())
    .build()
    .await?;

Accessing in a handler via engine.extension::<T>() — returns Option<Arc<T>>:

async fn my_handler(req: Request) -> Response {
    if let Some(mongo) = req.engine.extension::<mongodb::Client>() {
        let collection = mongo.database("mydb").collection::<Document>("users");
    }
    if let Some(redis) = req.engine.extension::<redis::Client>() {
        // ...
    }
}

engine.custom_db::<T>() is kept as a backward-compatible alias — both methods are equivalent.

Static files

let app = RuniqueApp::builder(config)
    .statics()     // Enable static files
    // or
    .no_statics()  // Explicitly disable
    .build()
    .await?;

Default values

ConfigurationDefaultNotes
Session duration24 hours
Session storeCleaningMemoryStore
CSRF protection✅ Always enabledCannot be disabled
Error handler✅ Enabled
CSP + security headers✅ Always onUnconditional, regardless of mode; .with_csp(...) customizes, doesn't enable
Host validation❌ DisabledNo .env variable controls it — call .with_allowed_hosts(...)
Cache control✅ EnabledNo-cache in debug; .with_cache(true) turns it off
Static files✅ Enabled.no_statics() to disable
Admin hot reloadFollows DEBUGAutomatic via is_debug()
Framework logs❌ DisabledEnable via .with_log(|l| ...)