Skip to main content

Axum Integration

toggly-axum provides native extractors and a Tower layer for Axum 0.7. Register the shared Arc<TogglyClient> as an Extension for these native surfaces; the optional typed state wrapper is a separate choice.

Installation​

This adapter uses Axum 0.7:

[dependencies]
toggly = "0.6"
toggly-axum = "0.6"
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }

Axum 0.8​

Use the separately named toggly-axum08 crate for Axum 0.8. It exposes the same Toggly layer, extractor and state imports under toggly_axum08:

[dependencies]
toggly = "0.6"
toggly-axum08 = "0.1"
axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
use toggly_axum08::{Feature, TogglyLayer, TogglyState};

Do not combine toggly-axum with Axum 0.8 or toggly-axum08 with Axum 0.7. Choose the adapter matching the application's Axum major; existing Axum 0.7 imports remain unchanged.

Setup​

Apply .layer(Extension(client.clone())) outside the routes that use native Feature/TogglyExtractor/TogglyLayer. Registering only .with_state(...) does not install this Extension.

Feature Extractor​

Feature reads the shared Extension and an identity header. Its is_enabled and is_disabled helpers return bool; evaluation errors become false. It also exposes context() and client().

Multiple Features​

Use feature.client().evaluate_gate(...) with an explicit context and Requirement::All or Any when combining keys. Gate negation applies to each result before combining.

TogglyExtractor​

For complete request context, use the native direct-client extractor:

use axum::{http::HeaderMap, http::StatusCode};
use toggly::{EvalContext, HttpRequestMapper};
use toggly_axum::TogglyExtractor;

pub async fn contextual(
headers: HeaderMap,
client: TogglyExtractor,
) -> Result<&'static str, StatusCode> {
// Example auth data: replace with values from your verified principal.
let base = EvalContext::builder().identity("alice").claim("role", "admin").build();
let mapped = headers.iter().filter_map(|(name, value)| {
value.to_str().ok().map(|value| (name.as_str(), value))
});
let context = HttpRequestMapper::merge_into(mapped, Some(&base));
match client.is_enabled("filter-user-claims", context).await {
Ok(true) => Ok("Condition matched"),
Ok(false) => Ok("Condition did not match"),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}

Add .route("/context", get(contextual)) to the complete Router below. Only trust country headers controlled by your proxy. This explicit evaluation does not change the contexts used by other native gates on the route.

Tower Layer​

Layer Options​

TogglyLayer::require(key) requires a true result; TogglyLayer::deny(key) requires a false result. .negate() toggles the condition and .identity_header(name) supplies the layer's identity source. The layer does not derive authenticated claims or entity context.

Layer Responses​

With its client Extension installed, the layer calls the handler for an allowed result and returns 404 for a denied result. Install the Extension as shown below and verify denied routes before deployment. Use your normal authorization independently.

State Wrapper​

TogglyState can be held in Axum typed state. Extract it through State<TogglyState>; the wrapper itself is not an extractor:

use axum::{extract::State, routing::get, Router};
use std::sync::Arc;
use toggly::{EvalContext, TogglyClient};
use toggly_axum::TogglyState;

async fn state_handler(State(state): State<TogglyState>) -> &'static str {
let context = EvalContext::with_identity("alice");
if state.is_enabled_with_context("new-dashboard", context).await {
"New dashboard"
} else {
"Classic dashboard"
}
}

pub fn state_routes(client: Arc<TogglyClient>) -> Router {
Router::new().route("/", get(state_handler))
.with_state(TogglyState::from_arc(client))
}

TogglyState::is_enabled(key) uses the default context; its explicit-context method is is_enabled_with_context. This example uses state-owned evaluation only. If adding native Feature or TogglyLayer routes, also install the shared client Extension.

Identity from Headers​

Feature reads x-user-id before x-identity. The layer first checks its configured identity header, then falls back to x-user-id, then x-identity when the preceding header is absent. These headers are not verified authentication. Supply trusted identity and claims from your auth boundary and prepare enriched context before the corresponding direct evaluation.

Nested Routers​

Build gated route groups with .route_layer(TogglyLayer::require(key)), then install the client Extension on the enclosing Router. Native helpers do not automatically consume arbitrary request extensions containing an EvalContext.

Complete Example​

Set TOGGLY_APP_KEY locally and create new-dashboard and api-v2 in Production. Save as src/main.rs; cargo run serves http://localhost:3000. Ctrl-C drains requests before closing the SDK.

use axum::{routing::get, Extension, Router};
use std::sync::Arc;
use toggly::TogglyClient;
use toggly_axum::{Feature, TogglyLayer};

async fn index(feature: Feature) -> &'static str {
if feature.is_enabled("new-dashboard").await {
"New dashboard"
} else {
"Classic dashboard"
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Bind first: a port error should not leave an initialized client behind.
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
let client = Arc::new(TogglyClient::builder()
.app_key(std::env::var("TOGGLY_APP_KEY")?)
.environment("Production").build().await?);
let app = Router::new()
.route("/", get(index))
.route("/api/v2", get(|| async { "API v2" })
.layer(TogglyLayer::require("api-v2").identity_header("x-user-id")))
.layer(Extension(client.clone()));
let result = axum::serve(listener, app)
.with_graceful_shutdown(async { let _ = tokio::signal::ctrl_c().await; })
.await;
client.close().await;
result?;
Ok(())
}

With Axum State​

Use an application's own state type for additional dependencies, and retain the same Arc<TogglyClient> in the native Extension. Avoid building a new SDK client per request. See the Samples catalog for setup recipes, and the evaluation guide for complete user/entity mapping.