Skip to content

Deployment and Authentication

The dashboard can run as a standalone process or as part of an existing axum application. Both forms use the same API and embedded assets.

Build the binary with the web feature.

Terminal window
cargo build --release --features web

Start the server from a TOML application configuration.

Terminal window
horsies web ./config/horsies.toml

You can also supply a database URL directly.

Terminal window
horsies web \
--database-url postgresql://app:secret@db.example.com/horsies \
--host 127.0.0.1 \
--port 8600

The standalone server creates an observe-only Horsies application. It does not run migrations. It does not run the task-history fleet gate. It does not register application tasks or workflows.

Reads work without a compiled registry. Workflow resume can require registered workflow definitions when a node uses args_from. Mount the router in the application process when actions need that registry.

Use a transaction-pool URL for queries and a direct or session-pool URL for LISTEN/NOTIFY.

Terminal window
horsies web \
--database-url postgresql://app:secret@pool.example.com/horsies \
--session-database-url postgresql://app:secret@db.example.com/horsies \
--pgbouncer-transaction-mode

--pgbouncer-transaction-mode requires both URLs. The session URL must preserve listener state.

OptionDefaultMeaning
CONFIGnoneTOML application configuration path
--database-url URLnoneRuntime query URL instead of CONFIG
--session-database-url URLnoneDirect or session URL for events
--pgbouncer-transaction-modefalseTreat the runtime URL as a transaction pool
--host HOST127.0.0.1Bind interface or host
--port PORT8600Bind port
--auth MODEnonenone or trusted-header
--trusted-header NAMEX-Forwarded-UserProxy identity header
--enable-actionsfalseEnable task and workflow actions
--custom-css-url URLnoneLoad a stylesheet after the bundled styles
--loglevel LEVELinfodebug, info, warning, or error

Invalid security arguments exit with code 2. Startup and server errors exit with code 1. A clean shutdown exits with code 0.

create_monitoring_router returns a normal axum Router. Nest it at any path.

use std::sync::Arc;
use axum::Router;
use horsies::web::{
create_monitoring_router, MonitoringUiConfig, ViewOnly,
};
use horsies::Horsies;
async fn router(app: &Horsies) -> Result<Router, horsies::AppError> {
let broker = app.get_broker().await?;
let monitoring = create_monitoring_router(
app,
Arc::clone(&broker),
ViewOnly,
MonitoringUiConfig::default(),
false,
);
Ok(Router::new().nest("/monitoring", monitoring))
}

The server injects the mount path into the SPA. API requests and assets remain relative to that path.

The router checks view authorization before any action-specific check. It then checks action authorization, the intent header, and schema compatibility.

PolicyReadsActions
ViewOnlyAllowedRefused
AllowAllAllowedAllowed when actions are enabled
TrustedHeaderRequires a non-empty identity headerAlso requires the policy to allow actions
Custom MonitoringAuthPolicyDefined by the hostDefined by the host

AllowAll means that the host application owns the authentication boundary. Do not expose an AllowAll router without that boundary.

Every action request must include this header:

X-Horsies-Intent: action

The intent header does not replace authentication. It is an extra guard for state-changing requests.

--auth trusted-header trusts the configured identity header. The reverse proxy MUST strip or overwrite that header on every incoming request. A proxy that forwards a client value makes the policy spoofable.

The CLI prints this warning on every trusted-header start.

The standalone CLI serves at the root path. Proxy it at the root path too.

location / {
proxy_set_header X-Forwarded-User $remote_user;
proxy_pass http://127.0.0.1:8600/;
}

The authentication layer before this block must set $remote_user. The proxy must not preserve a client-supplied X-Forwarded-User value.

Mount create_monitoring_router in an axum application when the dashboard needs a path prefix such as /horsies/.

--auth none is accepted only on localhost or a loopback IP address. Use a trusted proxy or a custom mounted policy for any network-reachable bind.

The schema probe uses catalog reads only. It never creates tables, applies a migration, or writes a cutover attestation.

The server can serve reads when the stored version differs from the expected version. It disables every action until the version matches and the task-history cutover attestation is present.

An absent schema and an unknown schema still serve the embedded shell. The UI shows the matching refusal state instead of issuing actions.

The CLI accepts one stylesheet URL.

Terminal window
horsies web ./config/horsies.toml \
--custom-css-url https://assets.example.com/horsies.css

Mounted deployments set the same value in MonitoringUiConfig.

use horsies::web::MonitoringUiConfig;
let ui = MonitoringUiConfig {
custom_css_url: Some("/assets/horsies.css".to_owned()),
};

The link appears after the bundled stylesheet. The browser loads it on each page. The server does not fetch, validate, or copy the stylesheet.

The production SPA is embedded in the Rust binary. No Node.js process is required at runtime.

One dashboard process owns one dedicated event-listener session. Size direct or session-pooled PostgreSQL capacity for that connection plus normal query traffic.

Run more than one dashboard process for process-level availability. Each process owns its own event listener and cache.