Axum
Integrate Better Auth with the Axum web framework.
Better Auth provides first-class integration with Axum via the axum feature flag, including automatic route mounting and schema-aware session extractors.
The canonical end-to-end example lives in examples/axum_server.rs. Generate src/auth_schema.rs with the quick-start guide before using the examples below.
Setup
[dependencies]
better-auth = { version = "1.0.0-alpha.3", git = "https://github.com/better-auth-rs/better-auth-rs", branch = "master", features = ["axum", "seaorm2"] }
axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"Mounting Auth Routes
Use .axum_router() when Arc<BetterAuth<YourSchema>> is the router state:
mod auth_schema;
use auth_schema::{AppAuthSchema, create_auth_tables};
use better_auth::{AuthConfig, BetterAuth};
use better_auth::plugins::{
EmailPasswordPlugin, SessionManagementPlugin,
PasswordManagementPlugin, AccountManagementPlugin,
};
use better_auth::integrations::axum::AxumIntegration;
use axum::Router;
use better_auth::seaorm::{Database, SeaOrmStore};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = AuthConfig::new("your-very-secure-secret-key-at-least-32-chars-long")
.base_url("http://localhost:8080");
let database = Database::connect("sqlite::memory:").await?;
create_auth_tables(&database).await?;
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), database);
let auth = Arc::new(
BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(EmailPasswordPlugin::new().enable_signup(true))
.plugin(SessionManagementPlugin::new())
.plugin(PasswordManagementPlugin::new())
.plugin(AccountManagementPlugin::new())
.build()
.await?
);
let auth_router = auth.clone().axum_router();
let app = Router::new()
.nest("/auth", auth_router)
.with_state(auth);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}Using Better Auth Inside Your AppState
For real applications, prefer axum_router_with_state::<AppState>() and let
Axum extract Arc<BetterAuth<AppAuthSchema>> from your application state via FromRef.
mod auth_schema;
use auth_schema::{AppAuthSchema, create_auth_tables};
use axum::extract::FromRef;
use axum::{Json, Router, routing::get};
use better_auth::{AuthConfig, BetterAuth};
use better_auth::integrations::axum::{AxumIntegration, CurrentSession};
use better_auth::plugins::{EmailPasswordPlugin, SessionManagementPlugin};
use better_auth::prelude::AuthUser;
use better_auth::seaorm::{Database, DatabaseConnection, SeaOrmStore};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
auth: Arc<BetterAuth<AppAuthSchema>>,
db: DatabaseConnection,
app_name: &'static str,
}
impl FromRef<AppState> for Arc<BetterAuth<AppAuthSchema>> {
fn from_ref(state: &AppState) -> Self {
state.auth.clone()
}
}
async fn profile(session: CurrentSession<AppAuthSchema>) -> Json<serde_json::Value> {
Json(serde_json::json!({
"id": session.user.id(),
"email": session.user.email(),
}))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database = Database::connect("sqlite::memory:").await?;
create_auth_tables(&database).await?;
let config = AuthConfig::new("your-very-secure-secret-key-at-least-32-chars-long")
.base_url("http://localhost:8080");
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), database.clone());
let auth = Arc::new(
BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(EmailPasswordPlugin::new().enable_signup(true))
.plugin(SessionManagementPlugin::new())
.build()
.await?,
);
let state = AppState {
auth: auth.clone(),
db: database,
app_name: "my-app",
};
let app = Router::new()
.route("/api/profile", get(profile))
.nest("/auth", auth.axum_router_with_state::<AppState>())
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}What Gets Mounted
.axum_router() automatically registers:
- All plugin routes (sign-up, sign-in, sessions, etc.)
GET /ok— health checkPOST /update-user— user profile updatesPOST /delete-user— account deletionPOST /change-email— email changes
With the router nested under /auth, endpoints become /auth/sign-up/email, /auth/get-session, etc.
Session Extractors
Better Auth provides Axum extractors that validate persisted sessions through the session manager. Expired or inactive sessions are rejected, and eligible sessions are refreshed. OptionalSession returns None for missing or invalid credentials and propagates database failures. API key session emulation applies to auth plugin routes; business routes that accept machine credentials must call API key verification explicitly.
CurrentSession — Require Authentication
Use CurrentSession<YourSchema> in your handler signature to require a valid session. Returns 401 Unauthorized automatically if no valid session is found.
use better_auth::integrations::axum::CurrentSession;
use better_auth::prelude::AuthUser; // trait for .id(), .email(), etc.
use axum::{Json, response::IntoResponse};
async fn get_profile(
session: CurrentSession<AppAuthSchema>,
) -> impl IntoResponse {
Json(serde_json::json!({
"id": session.user.id(),
"email": session.user.email(),
"name": session.user.name(),
}))
}CurrentSession provides two public fields:
| Field | Type | Description |
|---|---|---|
user | YourSchema::User | The authenticated app-owned user model |
session | YourSchema::Session | The current app-owned session model |
OptionalSession — Optional Authentication
Use OptionalSession<YourSchema> for routes that should work for both authenticated and anonymous users. Missing, expired, or invalid sessions produce None. Database and internal errors reject the request with an error response.
use better_auth::integrations::axum::OptionalSession;
use better_auth::prelude::AuthUser;
use axum::{Json, response::IntoResponse};
async fn home(
session: OptionalSession<AppAuthSchema>,
) -> impl IntoResponse {
let user_info = session.0.map(|s| {
serde_json::json!({
"id": s.user.id(),
"email": s.user.email(),
})
});
Json(serde_json::json!({
"message": "Welcome",
"user": user_info,
}))
}Token Extraction
Both extractors look for the session token in this order:
Authorization: Bearer <token>header- Session cookie (name from
SessionConfig::cookie_name, defaultbetter-auth.session_token)
Full Example
See examples/axum_server.rs for the complete working server, including:
- app-owned auth entity declarations with
AuthEntity - the
AppAuthSchemadefinition withAuthSchema FromRef<AppState> for Arc<BetterAuth<AppAuthSchema>>axum_router_with_state::<AppState>()CurrentSession<AppAuthSchema>andOptionalSession<AppAuthSchema>
Request/Response Conversion
The integration automatically converts between Axum and Better Auth types:
- Headers: All request headers are forwarded
- Body: Request body is read as bytes and passed through
- Query: Query parameters are parsed from the URL
- Status codes: Mapped directly to HTTP status codes
- Response headers: Set-Cookie and other headers are forwarded