# @suaveplan/websocket-gateway - Server-tier WebSocket gateway daemon — rooms, presence, and channel multiplexing over WebSocket connections. Idempotent start/stop lifecycle, injectable wsServerFactory, AbortSignal-aware, inline /health endpoints, OTel observability, callbacks for auth/authz/rate-limit/audit until those packages land . ## Summary Server-tier WebSocket gateway daemon with rooms, presence tracking, and channel-multiplexed pub/sub. Pairs with [`@suaveplan/network-transport-websockets`](../network-transport-websockets) on the browser side. ## Documentation tree - [channel](./src/channel/channel.md) — _(no submodules)_ - [channel](./src/channel/channel.md) — _(no submodules)_ - [errors](./src/errors/errors.md) — _(no submodules)_ - [errors](./src/errors/errors.md) — _(no submodules)_ - [gateway](./src/gateway/gateway.md) — _(no submodules)_ - heartbeat — Builds the heartbeat sweep that the dae… - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r… - [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann… - [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr… - [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien… - [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible… - [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo… - [gateway](./src/gateway/gateway.md) — _(no submodules)_ - heartbeat — Builds the heartbeat sweep that the dae… - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect… - [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi… - [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr… - [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink… - [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi… - [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc… - [health](./src/health/health.md) — _(no submodules)_ - [health](./src/health/health.md) — _(no submodules)_ - [otel](./src/otel/otel.md) — _(no submodules)_ - [otel](./src/otel/otel.md) — _(no submodules)_ - [presence](./src/presence/presence.md) — _(no submodules)_ - [presence](./src/presence/presence.md) — _(no submodules)_ - [room](./src/room/room.md) — _(no submodules)_ - [room](./src/room/room.md) — _(no submodules)_ - [types](./src/types/types.md) — _(no submodules)_ - [types](./src/types/types.md) — _(no submodules)_ - [channel](./src/channel/channel.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r… - [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann… - [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr… - [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien… - [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible… - [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo… - [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect… - [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi… - [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr… - [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink… - [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi… - [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc… - [health](./src/health/health.md) — _(no submodules)_- [health](./src/health/health.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [channel](./src/channel/channel.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r… - [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann… - [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr… - [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien… - [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible… - [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo… - [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect… - [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi… - [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr… - [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink… - [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi… - [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc… - [health](./src/health/health.md) — _(no submodules)_- [health](./src/health/health.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [channel](./src/channel/channel.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r… - [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann… - [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr… - [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien… - [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible… - [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo… - [gateway](./src/gateway/gateway.md) — _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae… - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect… - [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi… - [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr… - [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink… - [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi… - [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc… - [health](./src/health/health.md) — _(no submodules)_- [health](./src/health/health.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_- [channel](./src/channel/channel.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_ - [errors](./src/errors/errors.md) — _(no submodules)_- [gateway](./src/gateway/gateway.md) — _(no submodules)_ - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r… - [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann… - [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr… - [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien… - [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible… - [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo… - [gateway](./src/gateway/gateway.md) — _(no submodules)_ - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect… - [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi… - [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr… - [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink… - [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi… - [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc… - [health](./src/health/health.md) — _(no submodules)_ - [health](./src/health/health.md) — _(no submodules)_- [otel](./src/otel/otel.md) — _(no submodules)_ - [otel](./src/otel/otel.md) — _(no submodules)_- [presence](./src/presence/presence.md) — _(no submodules)_ - [presence](./src/presence/presence.md) — _(no submodules)_- [room](./src/room/room.md) — _(no submodules)_ - [room](./src/room/room.md) — _(no submodules)_- [types](./src/types/types.md) — _(no submodules)_ - [types](./src/types/types.md) — _(no submodules)_ ## Install ```bash bun add @suaveplan/websocket-gateway ``` ## Quick start ```ts import { WebSocketGateway } from "@suaveplan/websocket-gateway"; import { makeRealWsServerFactory } from "@suaveplan/websocket-gateway/tests/setup/real-ws-adapter"; const gateway = new WebSocketGateway({ port: 8080, host: "0.0.0.0", wsServerFactory: makeRealWsServerFactory(() => gateway.snapshot()), }); const { address, port } = await gateway.start(); console.log(`gateway listening on ws://${address}:${port}`); // Rooms gateway.createRoom({ id: "general", maxClients: 100 }); await gateway.joinRoom("general", "alice"); await gateway.broadcastToRoom("general", "alice", { text: "hi" }); // Channels await gateway.subscribeToChannel("events", "alice", "user.created"); await gateway.publishToChannel("events", "alice", "user.created", { id: "u1" }); // Graceful shutdown await gateway.stop(); ``` ## Architecture Five core subsystems, each in its own feature module: - `room` — capacity-controlled member sets with broadcast + history. - `presence` — online/away/busy/offline state with heartbeat sweep. - `channel` — pub/sub with `.`-delimited topic wildcards (`*`, `**`). - `gateway` — the orchestrator that owns the WS server, the gate pipeline, and idempotency. - `health` — inline `/health`, `/ready`, `/live` probes served from the same TCP port. The daemon binds an injectable `WsServerLike` adapter: production wires the real `ws.WebSocketServer`, tests bind a deterministic in-memory `FakeWsServer` so the no-mocks policy holds. ## Deferred integrations (replace these callbacks when packages land ) The proposal explicitly defers full integration with four upstream packages until they ship. Until then, the gateway accepts callbacks: | Callback | Default | Replace with | |---|---|---| | `authVerifier(handshake) => Promise` | anonymous pass-through | `@suaveplan/jwt-tenant` JWT verifier | | `authorizationCheck(ctx, action) => Promise` | always allow | `@suaveplan/permissions` RBAC/ABAC gate | | `rateLimitGate(ctx, action) => Promise` | always allow | `@suaveplan/rate-limiter` bucket gate | | `auditLogger(event) => void` | no-op | `@suaveplan/audit-logging` sink | Replacement is per-callback: when `@suaveplan/permissions` ships, swap `authorizationCheck` and leave the others alone. Public types (`AuthContext`, `GatewayAction`, `AuditEvent`) do not change. ```ts import { WebSocketGateway } from "@suaveplan/websocket-gateway"; const gateway = new WebSocketGateway({ port: 0, wsServerFactory: realFactory, authVerifier: async (handshake) => { // When @suaveplan/jwt-tenant lands, replace this body with: // return jwtTenant.verify(handshake.headers.authorization); return defaultAuthVerifier(handshake); }, }); ``` ## Direct integrations (already wired) These packages have shipped and the gateway uses them directly: - `@suaveplan/idempotency` — `IdempotencyGuard` wraps every mutating event (room create, broadcast, presence transition). Pass an `idempotencyKey` on `joinRoom` / `broadcastToRoom` to deduplicate at-least-once retries. - `@suaveplan/webhook-client` — `deliver()` ships every presence event (when `presenceWebhook` is configured) with HMAC-SHA-256 signing and retry budget. Failures are swallowed so presence is best-effort. - `@suaveplan/retry` — consumed transitively by `webhook-client` for exponential backoff + circuit breaking. - `@suaveplan/logger` — structured logging (peer dep; consumer wires its own logger). ## Observability OTel spans: `ws_gateway.{start, stop, connect, message, broadcast}`. Counters: `ws_gateway.rooms_active` (UpDown), `ws_gateway.presence_updates_total{kind}`, `ws_gateway.actions_total{action, outcome}`. Histograms: `ws_gateway.message_dispatch_ms`, `ws_gateway.start_duration_ms`. All instruments live in `./otel/otel.ts`. ## Configuration See `WebSocketGatewayConfig` in `src/types/types.ts` for the full surface. Highlights: - `port` / `host` — listener bind. `port: 0` for ephemeral. - `maxRooms` / `maxClientsPerRoom` — capacity caps; overflow throws `ROOM_FULL`. - `heartbeatIntervalMs` / `clientTimeoutMs` — heartbeat sweep cadence + timeout. - `enablePresence` / `enableChannels` — feature toggles. - `presenceWebhook` — optional signed webhook delivery for presence events. - `bus` — optional `EventBusLike` for typed observability. ## License MIT