> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lupin.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSockets

> Live event streams through the RealtimeHub Durable Object.

# WebSockets

```
GET /v0/websockets?api_key=…&inbox_ids=a,b&event_types=message.received
```

Authenticate with `Authorization: Bearer` or `?api_key=` (browsers can't set
headers). The protocol:

| Direction | Frame                                                                                  |
| --------- | -------------------------------------------------------------------------------------- |
| Client →  | `{"action":"hello"}` — server replies `ready` (send it; handshake frames can drop)     |
| Client →  | `{"action":"subscribe","inbox_ids":[],"event_types":[]}` — server replies `subscribed` |
| Client →  | `{"action":"ping"}` — server replies `pong`                                            |
| Server →  | `{"type":"event","event_type":"email.sent","event_id":"…",…}`                          |

```js theme={null}
const ws = new WebSocket(url);
ws.onopen = () => ws.send(JSON.stringify({ action: "hello" }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
```

## Architecture

Workers forbid cross-request socket I/O, so sockets live in the
**RealtimeHub Durable Object** (`apps/api/src/realtime/hub.ts`). Fetch
handlers publish through `hub.fetch(/publish)`; the hub broadcasts in its own
context. Without the `REALTIME_HUB` binding the route serves inline
(same-context only). Shard hubs per org when one gets hot.
