lilydocs
HTTP APIWebSocketConsumer
Broadcasting3 min read

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.

rooms.rs
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.

QUICK START
Documentation
Source code