Skip to content

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.

WsServer is generic over a zero-sized type:

nest_rs::ws::WsServer
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:

crates/features/src/notifications/ws/gateway.rs
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:

crates/features/src/notifications/ws/gateway.rs (from the demo)
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.

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:

crates/features/src/admin/notifier.rs
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:

crates/features/src/admin/module.rs
#[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 —

Terminal window
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.

Picture a real-time app with two concerns living side by side:

  • A chat gateway at /ws where users post messages and broadcasts shape the conversation. The handler stack uses rooms heavily.
  • A notification gateway at /notify where 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.

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:

Terminal window
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.

  • WsServer<N> — the generic, the Global default, the Registry trait that erases N on the client side.
  • 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.