Autumn's ws feature provides a named channel registry for WebSockets, SSE,
and server-rendered htmx fragments. Local development uses in-process
tokio::broadcast channels. Multi-replica deployments can switch the same
API to Redis pub/sub with autumn.toml.
Enable
[dependencies]
autumn-web = { version = "0.4", features = ["ws"] }
Publish
Use AppState::broadcast() when the payload is intended for browser clients.
publish sends raw UTF-8 text. publish_html wraps a Maud fragment in an
hx-swap-oob envelope for htmx.
use autumn_web::prelude::*;
#[post("/tasks/{id}/complete")]
async fn complete(state: AppState, Path(id): Path<i64>) -> AutumnResult<&'static str> {
state.broadcast().publish_html(
"tasks",
&html! {
li id={ "task-" (id) } class="done" { "complete" }
},
)?;
Ok("ok")
}
For protocol payloads that are already encoded, use publish:
# use autumn_web::prelude::*;
# fn publish(state: AppState) -> AutumnResult<()> {
state.broadcast().publish("tasks", br#"{"type":"task.completed"}"#.as_slice())?;
# Ok(())
# }
Subscribe with SSE
The one-line SSE primitive subscribes to a topic and emits each message as
SSE data.
use autumn_web::prelude::*;
#[get("/events")]
async fn events(State(state): State<AppState>) -> impl IntoResponse {
autumn_web::sse::stream(&state, "tasks")
}
Use stream_authorized when subscription needs access checks. The hook runs
before Autumn allocates the channel subscriber.
use autumn_web::prelude::*;
#[get("/events/private")]
async fn private_events(
State(state): State<AppState>,
session: Session,
) -> AutumnResult<impl IntoResponse> {
autumn_web::sse::stream_authorized(&state, "private-tasks", |_| async move {
if session.contains_key("user_id").await {
Ok(())
} else {
Err(AutumnError::unauthorized_msg("login required"))
}
})
.await
}
Direct Channels
AppState::channels() remains the low-level primitive for WebSocket loops and
custom transports.
# use autumn_web::prelude::*;
# async fn example(state: AppState) -> AutumnResult<()> {
let tx = state.channels().sender("lobby");
let mut rx = state.channels().subscribe_authorized("lobby", |_| async {
Ok::<(), AutumnError>(())
}).await?;
tx.send("hello")?;
let _ = rx.recv().await;
# Ok(())
# }
Redis Backend
Local is the default:
[channels]
backend = "in_process"
capacity = 32
Use Redis for multi-replica fan-out:
[channels]
backend = "redis"
capacity = 128
[channels.redis]
url = "redis://127.0.0.1:6379/"
key_prefix = "autumn:channels"
Equivalent environment overrides:
$env:AUTUMN_CHANNELS__BACKEND = "redis"
$env:AUTUMN_CHANNELS__CAPACITY = "128"
$env:AUTUMN_CHANNELS__REDIS__URL = "redis://127.0.0.1:6379/"
$env:AUTUMN_CHANNELS__REDIS__KEY_PREFIX = "autumn:channels"
The Redis backend publishes locally first, then relays the same envelope over Redis. Each process ignores messages carrying its own origin id, which avoids double-delivery on the publishing replica.
Custom Backends
Implement autumn_web::channels::ChannelsBackend and install it with
AppBuilder::with_channels_backend. This bypasses config-driven backend
selection, matching the session-store escape hatch.
# use autumn_web::prelude::*;
# fn configure(app: autumn_web::app::AppBuilder) -> autumn_web::app::AppBuilder {
app.with_channels_backend(LocalChannelsBackend::new(64))
# }
Actuator Metrics
With the ws feature, /actuator/channels returns per-topic metrics:
{
"channels": {
"tasks": {
"subscriber_count": 2,
"lifetime_publish_count": 17,
"dropped_count": 0,
"lagged_count": 1
}
}
}
dropped_count increments when a publish has no active local receivers.
lagged_count increments when slow subscribers skip messages from the
bounded ring buffer.
Two-Replica Smoke
The runnable examples/ws-echo app includes a Docker Compose smoke that is
usable in CI:
docker compose -f examples/ws-echo/docker-compose.yml up --build --abort-on-container-exit --exit-code-from smoke smoke
docker compose -f examples/ws-echo/docker-compose.yml down -v
The smoke container opens an SSE stream against one replica, publishes
through the other, and exits nonzero unless the first replica receives the
Maud-rendered list item as an hx-swap-oob fragment.
Manual equivalent while compose is running:
curl -N http://127.0.0.1:3001/events
curl -X POST http://127.0.0.1:3002/notify
Receiving the /notify list-item fragment on the 3001 stream proves Redis is
carrying channel events across replicas. A little ceremony, but at least the
abyss is observable.