Namespaces
WsServer<N> markers — multiple gateways with isolated connection registries on the same HTTP transport.
By default, every gateway in an app shares the same connection
registry: one WsServer, one pool of ConnIds, one set of rooms.
That’s the right answer most of the time — a chat gateway and an
admin-only gateway sitting on the same transport rarely need each
other’s broadcast. When they do need to be isolated — different
audiences, different room namespaces, different push surfaces — switch
the gateway to a private registry by giving it a namespace marker.
The marker pattern
Section titled “The marker pattern”WsServer is generic over a zero-sized type:
pub struct WsServer<N: 'static = Global> { /* ... */ }Global is the default. The container keys providers by type, so
WsServer<Global> and WsServer<MyNs> are wholly separate singletons:
distinct connection maps, distinct room sets, distinct push surfaces.
A marker is just a unit struct — no traits, no methods, no runtime
data:
pub struct NotificationsNs;Attach it to a gateway with #[gateway(namespace = NotificationsNs)]. The
registry comes from WsModule — the same module that provides the default one,
so namespacing changes which registry is wired, not how it is wired:
use nest_rs::ws::{WsClient, gateway, messages};
use crate::authn::AuthnGuard;
pub struct NotificationsNs;
#[gateway(path = "/notify", namespace = NotificationsNs)]#[use_guards(AuthnGuard)]#[derive(Default)]pub struct NotificationsGateway;
#[messages]impl NotificationsGateway { #[subscribe_message("ping")] #[public] async fn ping(&self, client: &WsClient) { // … }}That gateway’s WsClient holds an Arc<WsServer<NotificationsNs>> behind a
type-erased Registry trait. Calls to client.broadcast(...) reach
only NotificationsGateway connections; the ChatGateway next to it on the
same HTTP transport stays untouched.
Reaching the namespaced registry
Section titled “Reaching the namespaced registry”A service that wants to push to one namespace injects the typed registry directly — the marker is exported by the edge that declares it, so nothing but the type crosses the module boundary:
use std::sync::Arc;
use nest_rs::core::injectable;use nest_rs::ws::WsServer;
use crate::notifications::NotificationsNs;
#[injectable]pub struct AdminNotifier { #[inject] server: Arc<WsServer<NotificationsNs>>,}
impl AdminNotifier { pub fn shout(&self, text: &str) { let _ = self.server.broadcast("alert", &text); }}Its module imports WsModule, exactly as it would to inject the default
registry:
#[module(imports = [WsModule], providers = [AdminNotifier])]pub struct AdminModule;Arc<WsServer<Global>> and Arc<WsServer<NotificationsNs>> are distinct
injected types, and the access graph verifies each one is reachable on its own:
forget the import and the boot fails naming both the key and its owner —
Error: module access violation: `AdminNotifier` (in module `AdminModule`) depends on`WsServer<NotificationsNs>`, but `AdminModule` imports no module that provides it.`WsServer<NotificationsNs>` is provided by `WsModule` — add `WsModule` to`#[module(imports = [...])]` of `AdminModule`, …A service can inject both registries at once if it needs to fan out to both
audiences, and one WsModule import covers both.
Why namespaces exist
Section titled “Why namespaces exist”Picture a real-time app with two concerns living side by side:
- A chat gateway at
/wswhere users post messages and broadcasts shape the conversation. The handler stack uses rooms heavily. - A notification gateway at
/notifywhere the only purpose is server→client pushes for moderator alerts. Every connected client receives every notification; rooms would be noise.
On one shared WsServer, a server.broadcast("alert", ...) from a
notifications handler would also reach every chat user — the notification
arrives in the wrong client’s frame loop. Two namespaces solve it
structurally: distinct registries mean a broadcast on one is invisible
to the other, and no handler can leak a frame across by accident.
A ConnId is allocated from the namespace’s own counter, so the same
integer can identify two different connections across two registries.
This is exactly why WsClient holds the registry as a type-erased
dyn Registry — the N never surfaces on the handler API, but the
runtime always dispatches to the right pool.
The mount itself
Section titled “The mount itself”A namespaced gateway still self-mounts on the HTTP transport — same port, same CORS, same TLS. The only thing that changes is which registry the macro wires:
2026-06-08T10:23:14Z INFO nest_rs::routes: mounted endpoint kind="ws" path="/ws"2026-06-08T10:23:14Z INFO nest_rs::routes: mounted endpoint kind="ws" path="/notify"Listing both gateways’ modules in AppModule is enough, provided each of those
modules imports WsModule — the gateway’s registry is a declared dependency
like any #[inject] field, so a missing import is a boot error naming it rather
than a surprise at the first broadcast.
Reference
Section titled “Reference”WsServer<N>— the generic, theGlobaldefault, theRegistrytrait that erasesNon the client side.
Going further
Section titled “Going further”- Server-side push — once you have the typed registry, push it from anywhere DI reaches.
- Rooms — most of the time, the right answer to “I need a separate channel” is a room, not a namespace.