# WebSocket

> Nitro provides cross-platform WebSocket support powered by [CrossWS](https://crossws.h3.dev/) and [H3](https://h3.dev/).

WebSocket enables real-time, bidirectional communication between client and server. Nitro's WebSocket integration works across all supported deployment targets including Node.js, Bun, Deno, and Cloudflare Workers.

<read-more></read-more>

## Enable WebSocket

Enable WebSocket support in your Nitro configuration:

<code-group>

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  features: {
    websocket: true,
  },
});
```
</code-group>

## Usage

Create a WebSocket handler using `defineWebSocketHandler` and export it from a route file. WebSocket handlers follow the same [file-based routing](/docs/routing) as regular request handlers.

```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    console.log("Connected:", peer.id);
  },
  message(peer, message) {
    console.log("Message:", message.text());
    peer.send("Hello from server!");
  },
  close(peer, details) {
    console.log("Disconnected:", peer.id, details.code, details.reason);
  },
  error(peer, error) {
    console.error("Error:", error);
  },
});
```

<tip>

You can use any route path for WebSocket handlers. For example, `routes/chat.ts` handles WebSocket connections on `/chat`.
</tip>

### Connecting from the client

Use the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) to connect:

```js
const ws = new WebSocket("ws://localhost:3000/_ws");

ws.addEventListener("open", () => {
  console.log("Connected!");
  ws.send("Hello from client!");
});

ws.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});
```

## Hooks

WebSocket handlers accept the following lifecycle hooks:

### `upgrade`

Called before the WebSocket connection is established. Use it to authenticate requests, set the namespace, or attach context data to the peer.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    const url = new URL(request.url);
    const token = url.searchParams.get("token");
    if (!isValidToken(token)) {
      throw new Response("Unauthorized", { status: 401 });
    }
    return {
      context: { userId: getUserId(token) },
    };
  },
  open(peer) {
    console.log("User connected:", peer.context.userId);
  },
  // ...
});
```

The `upgrade` hook can return an object with:

| Property | Type | Description |
| --- | --- | --- |
| `headers` | `HeadersInit` | Response headers to include in the upgrade response |
| `namespace` | `string` | Override the pub/sub namespace for this connection |
| `context` | `object` | Data attached to `peer.context` |

Throw a `Response` to reject the upgrade.

### `open`

Called when a WebSocket connection is established and the peer is ready to send and receive messages.

```ts
open(peer) {
  peer.send("Welcome!");
}
```

### `message`

Called when a message is received from a peer.

```ts
message(peer, message) {
  const text = message.text();
  const data = message.json();
}
```

### `close`

Called when a WebSocket connection is closed. Receives a `details` object with optional `code` and `reason`.

```ts
close(peer, details) {
  console.log(`Closed: ${details.code} - ${details.reason}`);
}
```

### `error`

Called when an error occurs on the WebSocket connection.

```ts
error(peer, error) {
  console.error("WebSocket error:", error);
}
```

## Peer

The `peer` object represents a connected WebSocket client. It is available in all hooks except `upgrade`.

### Properties

| Property | Type | Description |
| --- | --- | --- |
| `id` | `string` | Unique identifier for this peer |
| `namespace` | `string` | Pub/sub namespace this peer belongs to |
| `context` | `object` | Arbitrary context data set during `upgrade` |
| `request` | `Request` | The original upgrade request |
| `peers` | `Set<Peer>` | All connected peers in the same namespace |
| `topics` | `Set<string>` | Topics this peer is subscribed to |
| `remoteAddress` | `string?` | Client IP address (adapter-dependent) |
| `websocket` | `WebSocket` | The underlying WebSocket instance |

### Methods

#### `peer.send(data, options?)`

Send a message directly to this peer. Accepts strings, objects (serialized as JSON), or binary data.

```ts
peer.send("Hello!");
peer.send({ type: "greeting", text: "Hello!" });
```

#### `peer.subscribe(topic)`

Subscribe this peer to a pub/sub topic.

```ts
peer.subscribe("notifications");
```

#### `peer.unsubscribe(topic)`

Unsubscribe this peer from a topic.

```ts
peer.unsubscribe("notifications");
```

#### `peer.publish(topic, data, options?)`

Broadcast a message to all peers subscribed to a topic within the same namespace. The publishing peer does **not** receive the message.

```ts
peer.publish("chat", { user: "Alice", text: "Hello everyone!" });
```

#### `peer.close(code?, reason?)`

Gracefully close the WebSocket connection.

```ts
peer.close(1000, "Normal closure");
```

#### `peer.terminate()`

Immediately terminate the connection without sending a close frame.

## Message

The `message` object in the `message` hook provides methods to read the incoming data in different formats.

| Method | Return Type | Description |
| --- | --- | --- |
| `text()` | `string` | Message as a UTF-8 string |
| `json()` | `T` | Message parsed as JSON |
| `uint8Array()` | `Uint8Array` | Message as a byte array |
| `arrayBuffer()` | `ArrayBuffer` | Message as an ArrayBuffer |
| `blob()` | `Blob` | Message as a Blob |

```ts
message(peer, message) {
  // Parse as text
  const text = message.text();

  // Parse as typed JSON
  const data = message.json<{ type: string; payload: unknown }>();
}
```

## Pub/Sub

Pub/sub (publish/subscribe) enables broadcasting messages to groups of connected peers through topics. Peers subscribe to topics and receive messages published to those topics.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.subscribe("chat");
    peer.publish("chat", { system: `${peer} joined the chat` });
    peer.send({ system: "Welcome to the chat!" });
  },
  message(peer, message) {
    // Broadcast to all other subscribers
    peer.publish("chat", {
      user: peer.toString(),
      text: message.text(),
    });
    // Echo back to sender
    peer.send({ user: "You", text: message.text() });
  },
  close(peer) {
    peer.publish("chat", { system: `${peer} left the chat` });
  },
});
```

<note>

`peer.publish()` sends the message to all subscribers of the topic **except** the publishing peer. Use `peer.send()` to also send to the publisher.
</note>

### Namespaces

Namespaces provide isolated pub/sub groups for WebSocket connections. Each peer belongs to one namespace, and `peer.publish()` only broadcasts to peers within the same namespace.

By default, the namespace is derived from the request URL pathname. This works naturally with [dynamic routes](/docs/routing#dynamic-routes) — each path gets its own isolated namespace:

```ts [routes/rooms/[room].ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.subscribe("messages");
    peer.publish("messages", `${peer} joined ${peer.namespace}`);
  },
  message(peer, message) {
    // Only reaches peers in the same room
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

In this example, clients connecting to `/rooms/game` are isolated from clients connecting to `/rooms/lobby` — each path is its own namespace.

To override the default namespace, return a custom `namespace` from the `upgrade` hook:

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    // Group connections by a query parameter instead of the pathname
    const url = new URL(request.url);
    const channel = url.searchParams.get("channel") || "general";
    return {
      namespace: `chat:${channel}`,
    };
  },
  open(peer) {
    peer.subscribe("messages");
    peer.publish("messages", `${peer} joined`);
  },
  message(peer, message) {
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

## Server-Sent Events (SSE)

[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) provide a simpler alternative when you only need server-to-client streaming. Unlike WebSockets, SSE uses standard HTTP and supports automatic reconnection.

```ts [routes/sse.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);

  const interval = setInterval(async () => {
    await stream.push(`Message @ ${new Date().toLocaleTimeString()}`);
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

Connect from the client using the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource):

```js
const source = new EventSource("/sse");

source.onmessage = (event) => {
  console.log(event.data);
};
```

### Structured messages

SSE messages support optional `id`, `event`, and `retry` fields:

```ts [routes/events.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);
  let id = 0;

  const interval = setInterval(async () => {
    await stream.push({
      id: String(id++),
      event: "update",
      data: JSON.stringify({ value: Math.random() }),
      retry: 3000,
    });
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

<read-more></read-more>
