Pub / Sub

crossws supports native pub-sub API integration. A peer can be subscribed to a set of named topics using peer.subscribe(<name>). Messages can be published to a topic using peer.publish(<name>, <message>). When publishing to a topic, the peer that does the publish is excluded from the broadcast so it won't receive its own message.

Self-exclusion is always in effect, on every adapter: the publishing peer never receives its own message back. Adapters implement this in one of two equivalent ways — some (Node, Deno, SSE) explicitly filter the publisher out before sending, while others (Bun, μWebSockets) delegate straight to the runtime's native publish(), which already excludes the sender by its own documented pub/sub semantics. The observable behavior is identical either way.
import { defineHooks } from "crossws";

const hooks = defineHooks({
  upgrade(req) {
    return {
      // namespace: new URL(req.url).pathname
    };
  },
  open(peer) {
    // Send welcome to the new client
    peer.send("Welcome to the server!");

    // Join new client to the "chat" topic
    peer.subscribe("chat");

    // Notify every other connected client
    peer.publish("chat", `[system] ${peer} joined!`);
  },

  message(peer, message) {
    // The server re-broadcasts incoming messages to everyone
    peer.publish("chat", `[${peer}] ${message}`);
  },

  close(peer) {
    peer.publish("chat", `[system] ${peer} has left the chat!`);
    peer.unsubscribe("chat");
  },
});

By default pub/sub is in-memory and local to one instance. To span a cluster, relay messages over a shared backplane with a sync adapter.

Read more in Guide > Sync.

Publishing from outside a peer

peer.publish() is convenient inside a hook, but sometimes you need to broadcast from somewhere that isn't tied to any single connection: an HTTP route, a cron job, or a webhook handler. The object returned by an adapter factory (e.g. nodeAdapter(...), or the instance backing crossws/adapters/cloudflare) exposes this same capability directly, alongside a read-only peers map of every currently connected peer:

import nodeAdapter from "crossws/adapters/node";

const ws = nodeAdapter({ hooks: { message() {} } });

// Broadcast to every peer subscribed to "chat", in every namespace
ws.publish("chat", "Server is restarting for maintenance");

// Inspect connected peers (Map<namespace, Set<Peer>>)
for (const [namespace, peers] of ws.peers) {
  console.log(namespace, peers.size);
}

publish(topic, message, { compress?, namespace? })

  • namespace: scopes delivery to peers in that namespace only. Omit it for a global publish that reaches subscribers across every namespace.
  • Timing: delivery to local subscribers is fully synchronouspublish() sends to every matching local peer before it returns, and it does not return a promise. The only asynchronous, best-effort part is relaying to other instances over an optional sync backplane; that relay is fire-and-forget and never throws out of publish(), so a flaky backplane can't block or crash the caller.
Read more in Guide > Sync.