Rooms
Establish authorized room membership and broadcast to room recipients in the same namespace.
A room is a broadcast group of connections within the same namespace. Its name is not proof of permission; application authorization policy decides who may join and publish.
Lobby example#
The example uses ChatWriteGuard. Add src/middleware.rs and src/rooms.rs to the application; identity middleware must publish chat:write through a real verifier. Because the guard is registered on the controller, do not additionally register the same type globally.
use std::sync::Arc;
use lilyrs::websocket::{Ack, Extensions, NoReply, Payload, WebSocketActionError,
WebSocketContext, WebSocketController, WebSocketControllerInitError,
WebSocketControllerTrait, async_trait, websocket_controller};
use serde::{Deserialize, Serialize};
use crate::middleware::ChatWriteGuard;
#[derive(Deserialize, Serialize)]
struct RoomMessage { text: String }
#[derive(WebSocketController)]
#[namespace("chat")]
#[guard(ChatWriteGuard)]
struct ChatController;
#[async_trait]
impl WebSocketControllerTrait for ChatController {
async fn new(_: Arc<Extensions>) -> Result<Self, WebSocketControllerInitError> { Ok(Self) }
}
#[websocket_controller]
impl ChatController {
#[message("join")]
async fn join(&self, context: WebSocketContext) -> Result<Ack<bool>, WebSocketActionError> {
// In this example, the chat:write permission only grants access to the fixed lobby room.
context.rooms().join("lobby").await.map_err(WebSocketActionError::internal)?;
Ok(Ack::new(true))
}
#[message("publish")]
async fn publish(&self, context: WebSocketContext, Payload(input): Payload<RoomMessage>)
-> Result<NoReply, WebSocketActionError>
{
context.clients().room("lobby").send("chat:received", input).await
.map_err(WebSocketActionError::internal)?;
Ok(NoReply)
}
}Download the complete example.
The client sends an ACK ID for chat:join. chat:publish sends an event to lobby recipients and returns NoReply without another terminal response. If the publisher is not a room member, it must join to receive broadcasts in this example; chat:write grants publish permission for the fixed lobby here.
Authorize dynamic rooms#
In a real application, if the room name comes from a DTO, verify tenant, project, or object access first. tenant-42-orders is a valid name; tenant/42:orders violates route-token grammar. A room name is at most 128 ASCII bytes and can contain letters/digits/-/_/..
Envelope room metadata does not automatically create a join, subscription, or guard. rooms().join changes membership only when the application explicitly calls it. Include when to remove membership after permission revocation in your domain policy.
Membership and snapshots#
Use rooms().join/leave for the caller and join_connection/leave_connection for an explicit connection in the same namespace. An explicit ID cannot cross into another namespace. rooms().snapshot(name) shows local membership; it is not a global presence view even with a Redis backplane enabled.
Disconnect cleanup removes memberships. The default per-connection room limit is 128. The same recipient in a clients().rooms(names) union is targeted once; explicit room/ID/exclusion lists are limited to 256 entries.
Broadcast result#
A successful room send means targets met the required queue/publish acceptance conditions. It does not prove every user saw the message or that an offline user will receive it later. Use receipts for partial results and a backplane for multiple nodes.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.