Hooks
Crossws provides a cross-platform API to define WebSocket servers. An implementation with these hooks works across runtimes without needing you to go into details of each of them. You only define the life-cycle hooks that you only need.
defineHooks() wrapper is optional and for type support and code auto completion.import { defineHooks } from "crossws";
const hooks = defineHooks({
upgrade(req) {
console.log(`[ws] upgrading ${req.url}...`);
return {
// namespace: new URL(req.url).pathname
headers: {},
};
},
open(peer) {
console.log(`[ws] open: ${peer}`);
},
message(peer, message) {
console.log("[ws] message", peer, message);
if (message.text().includes("ping")) {
peer.send("pong");
}
},
close(peer, details) {
console.log("[ws] close", peer, details);
},
error(peer, error) {
console.log("[ws] error", peer, error);
},
drain(peer) {
// Send buffer drained after backpressure — safe to resume sending.
// Pair with `peer.bufferedAmount`. Not all adapters emit this.
console.log("[ws] drain", peer);
},
ping(peer, data) {
// An application-level ping control frame arrived from the peer.
// Not all adapters emit this.
console.log("[ws] ping", peer, data);
},
pong(peer, data) {
// An application-level pong control frame arrived from the peer,
// typically in reply to `peer.ping()`. Not all adapters emit this.
console.log("[ws] pong", peer, data);
},
});
Context
You can attach data to the connection by returning a context object from the upgrade hook. This data is then available as peer.context in all other hooks for the lifetime of the connection.
cloudflare-durable.import { defineHooks } from "crossws";
const hooks = defineHooks({
upgrade(req) {
return {
context: { data: "myData" },
};
},
open(peer) {
console.log(peer.context.data); // myData
},
message(peer, message) {
console.log(peer.context.data); // myData
},
close(peer, details) {
console.log(peer.context.data); // myData
},
});
Authentication
During the upgrade hook it is possible to authenticate the user before upgrading the connection. If the user is not authenticated, you can return (or throw) a Response to prevent the connection from being upgraded.
import { defineHooks } from "crossws";
const hooks = defineHooks({
upgrade(req) {
const authHeader = req.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Basic ")) {
return new Response("Unauthorized", {
status: 401,
headers: {
"WWW-Authenticate":
'Basic realm="Websocket Authentication", charset="UTF-8"',
},
});
}
const base64Credentials = authHeader.split(" ")[1];
const [username, password] = atob(base64Credentials).split(":");
if (username !== "myUsername" || password !== "myPassword") {
return new Response("Unauthorized", {
status: 401,
headers: {
"WWW-Authenticate":
'Basic realm="Websocket Authentication", charset="UTF-8"',
},
});
}
return {
headers: {}, // Optionally return custom headers
};
},
});
Subprotocol negotiation
When a client opens a connection with subprotocols (new WebSocket(url, ["graphql-transport-ws"])), it sends them in the Sec-WebSocket-Protocol request header. Browsers reject the connection unless the server's handshake response accepts one of the offered subprotocols. By default crossws accepts none — a server never claims to speak a protocol you didn't opt into — so you have to select one.
Return protocol from the upgrade hook to accept a subprotocol for that connection. It should be one of the values the client offered.
import { defineHooks } from "crossws";
const hooks = defineHooks({
upgrade(req) {
const offered = req.headers.get("sec-websocket-protocol"); // "graphql-transport-ws, graphql-ws"
if (offered?.split(",").some((p) => p.trim() === "graphql-transport-ws")) {
return { protocol: "graphql-transport-ws" };
}
},
});
For a global default that applies to every connection, use the handleProtocols adapter option instead. It receives the set of offered subprotocols and returns the one to accept (or false for none); a protocol returned from the upgrade hook takes precedence over it.
crossws({
handleProtocols: (protocols) =>
protocols.has("graphql-transport-ws") ? "graphql-transport-ws" : false,
hooks,
});