Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Site

Site is the built Vyuh application. It owns configuration, the router, database pool, authenticator, template engine, task dispatcher, signal and emitter engines, services, commands, logging, and shutdown coordination.

Most applications interact with Site in two places:

  • At startup, through Site::build, Site::run, Site::serve, or Site::test.
  • Inside handlers and workers, where Site or subsystem handles can be extracted when framework access is needed.

Overview

The main public pieces are:

  • SiteConf for application configuration.
  • Site::build(conf, bundle) for building a site without serving it.
  • Site::run(conf, bundle) for command-aware application entrypoints.
  • Site::serve(conf, bundle) for directly building and serving HTTP.
  • Site::test(conf, bundle, pool) for tests with an explicit SQLx pool.
  • site.start() for serving an already-built site.
  • Site accessors such as db(), tasks(), templates(), service(), auth(), signals(), and reverse().
  • vyuh::testing::router(&site) for tests or Axum interop.
  • SiteConf::http(...) for global HTTP middleware and slash behavior.
  • SiteConf::templates(...) for Minijinja environment behavior.

Site is cheap to clone. Clones share the same underlying application state.

Configuration

Start from SiteConf::default() and set only what the application needs:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;
use vyuh::db::DbConf;
use vyuh::console::ConsoleConf;
use vyuh::file_storage::UploadConf;
use vyuh::middlewares::{HttpConf, TraceConf};
use vyuh::templates::{TemplateConf, TemplateDateFormats};

let conf = SiteConf::default()
    .host("127.0.0.1")
    .port(8080)
    .project_dir(".")
    .database(DbConf::from_url("sqlite://app.db?max=5")?)
    .secret_key("replace-with-a-long-random-secret")
    .templates(TemplateConf {
        date_formats: TemplateDateFormats {
            date: "%d %b %Y".into(),
            time: "%H:%M".into(),
            datetime: "%d %b %Y, %H:%M".into(),
        },
        ..TemplateConf::default()
    })
    .http(HttpConf {
        trace: TraceConf { enabled: true },
        ..HttpConf::default()
    })
    .uploads(UploadConf {
        dir: "media/uploads".into(),
        base_url: Some("/media/uploads".into()),
        ..UploadConf::default()
    })
    .console(ConsoleConf::default())
    .timezone("UTC");
}

project_dir is the base for relative media, upload, reload, auth key, and log paths. Static files and templates belong to bundles through asset dirs. SiteConf::validate() checks required fields and path readability before the site is built.

With no database backend feature enabled, SiteConf::default() uses a shared in-memory SQLite database URL and tasks use MemoryTaskStore. This is intended for quick starts, docs, local experiments, and tests. Production applications should enable exactly one backend feature (postgres, mysql, or sqlite) and configure a durable database.

For global HTTP behavior, see Middlewares. For Minijinja environment behavior and formatting helpers, see Templates. For upload storage, see Uploads. For optional operational inspection, see Console.

Environment helpers are available when configuration should come from the process environment:

#![allow(unused)]
fn main() {
let conf = vyuh::SiteConf::from_env_with_files()?;
}

from_env_with_files() loads .env, then .env.test, .env.dev, or .env.prod depending on the build mode. Environment variables currently patch common deployment fields such as DATABASE_URL, SECRET_KEY, HOST, PORT, TZ, and LOG_INIT.

Lifecycle

Vyuh keeps lifecycle on Site:

MethodPurpose
Site::buildbuild the site object without starting HTTP
Site::runcommand-aware application entrypoint; no args defaults to serve
Site::servebuild and directly serve HTTP, ignoring commands
Site::testbuild a test site with an explicit SQLx pool
site.startserve an already-built site

Use Site::run for ordinary application binaries:

use vyuh::prelude::*;

#[tokio::main]
async fn main() -> Result<(), vyuh::SiteError> {
    let bundle = bundles::bundle! {
        // routes, services, tasks, signals, assets, commands
    };

    vyuh::Site::run(SiteConf::from_env_with_files()?, bundle).await
}

Use Site::serve when a binary should ignore commands and only serve HTTP:

#![allow(unused)]
fn main() {
vyuh::Site::serve(SiteConf::from_env_with_files()?, app_bundle()).await?;
}

Use Site::build when the caller needs the site before serving, for example to inspect configuration, run setup code, or pass the built site to another runtime:

#![allow(unused)]
fn main() {
let site = vyuh::Site::build(conf, bundle).await?;
site.start().await?;
}

When arguments are supplied, Site::run executes the requested command:

#[tokio::main]
async fn main() -> Result<(), vyuh::SiteError> {
    vyuh::Site::run(vyuh::SiteConf::from_env_with_files()?, app_bundle()).await
}

During build, Vyuh validates configuration and bundles, builds the router, creates the database pool, loads templates, initializes services, registers OpenAPI endpoints, prepares task stores when tasks are present, and starts background engines.

Using Site In Handlers

Handlers can extract Site directly:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

#[bundles::route(path = "/health")]
async fn health(site: Site) -> Json<String> {
    Json(site.timezone().to_string())
}
}

Prefer subsystem handles for subsystem-specific work:

#![allow(unused)]
fn main() {
let db = site.db();
let templates = site.templates();
let tasks = site.tasks();
let auth = site.auth();
let counter = site.service::<CounterService>()?;
}

Task submission should go through site.tasks().submit(...) or site.tasks().submit_with(...). Template rendering should usually go through site.templates().render(...) or the Templates route extractor.

Error Rendering

Route parse errors, validation errors, auth failures, database errors, template errors, and application vyuh::Error values are normalized into ErrorReport before they are rendered. The default response is JSON-first. See Errors for the application/subsystem/rendered error model.

Applications can replace error rendering with SiteConf::errors(...):

#![allow(unused)]
fn main() {
use vyuh::prelude::*;
use vyuh::errors::ErrorConf;

let conf = SiteConf::default().errors(
    ErrorConf::default().handler(|ctx, report| async move {
        (
            report.status,
            [("content-type", "application/json")],
            serde_json::json!({
                "path": ctx.path,
                "code": report.code,
                "detail": report.detail,
            })
            .to_string(),
        )
            .into_response()
    }),
);
}

The handler is async and receives request context plus the normalized report, so applications can render templates, add headers, or choose a different content type.

Routing And Reverse URLs

Raw Axum router access is intentionally not part of the normal application lifecycle. Use Site::serve or site.start() for serving. Use vyuh::testing::router(&site) only for tests or interop that truly needs an Axum Router.

Named routes can be reversed through Site::reverse:

#![allow(unused)]
fn main() {
let url = site.reverse("user_detail", &[("id", "42")]);
}

reverse returns None when the route name or required parameters do not match a registered route.

Testing

Use Site::test when a test should build the real site with a caller-provided SQLx pool:

#![allow(unused)]
fn main() {
#[sqlx::test]
async fn route_works(pool: vyuh::db::Pool) -> Result<(), vyuh::SiteError> {
    let site = vyuh::Site::test(vyuh::SiteConf::default(), app_bundle(), pool).await?;
    let app = vyuh::testing::router(&site);
    Ok(())
}
}

For route-level tests, build a site and send requests through vyuh::testing::TestClient or vyuh::testing::router(&site). Use .log_init(false) in tests when test output should stay quiet.

Shutdown

Site owns a shared shutdown notifier. Long-lived service workers and other background loops should observe site.shutdown_notifier() and exit when it is notified.

#![allow(unused)]
fn main() {
let shutdown = site.shutdown_notifier();
tokio::select! {
    _ = shutdown.notified() => {}
    _ = do_work() => {}
}
}

Site::serve and site.start() install bounded graceful server shutdown. The first Ctrl+C starts graceful shutdown and prints a message explaining that a second Ctrl+C will force shutdown. SIGTERM, touch-reload, and site.shutdown() also start graceful shutdown. If active requests do not drain within conf.http.shutdown.grace_period_ms, Vyuh forces server shutdown and returns from Site::serve or site.start().

Channel transports are shutdown-aware: SSE streams end, WebSockets close, and long-poll requests return promptly when shutdown starts. Long-lived service workers should still select on site.shutdown_notifier() so they can stop their own work cleanly before the grace period expires.

shutdown_and_wait() can be used by tests or embedding code that needs to notify background tasks and abort remaining join handles.

Failure Modes

  • Invalid configuration returns SiteError::ConfError.
  • Database pool setup returns SiteError::DatabaseError.
  • Bundle validation and duplicate registration errors return SiteError::BundleError.
  • Template loading errors return SiteError::TemplateError.
  • Service construction errors return SiteError::ServiceError.
  • Task store migration errors return SiteError::TaskMigrationError.
  • Server bind or runtime errors return SiteError::IOError or SiteError::ServeError.

Current Limitations

  • Site is an in-process application handle, not a distributed coordinator.
  • Background engines are tied to the process that built the site.
  • Site::test uses the supplied pool but does not replace application-level schema setup; tests still need the schema their routes and services expect.