Architecture
Nuxt Realtime connects four systems together:
- Nitro - Nuxt's server engine, handling all HTTP and WebSocket traffic
- h3 / crossws - The lower-level frameworks Nitro builds on (h3 for HTTP, crossws for WebSockets)
- Engine.IO - Socket.IO's transport layer, which works directly with raw Node.js primitives
- Socket.IO - The application-level library that gives us rooms, events, and acknowledgements
The tricky part is that Nitro wraps Node.js primitives in its own abstractions, but Engine.IO and Socket.IO expect raw Node.js objects. The server plugin bridges that gap.
For how cross-server sync works on top of this, see Notification Layer and Multi-Server Sync.
Connection flow
The bridge layer
Everything goes through a route handler registered at /socket.io/. It's an h3 event handler with two hooks: one for HTTP, one for WebSocket:
nitroApp.router.use('/socket.io/', defineEventHandler({
handler(event) {
// HTTP requests (long-polling transport)
// Unwrap h3's H3Event to get the raw Node.js req/res
engine.handleRequest(event.node.req, event.node.res)
event._handled = true
},
websocket: {
open(peer) {
// WebSocket connections
// Unwrap crossws's Peer to get the raw Node.js request and socket
engine.prepare(peer._internal.nodeReq)
engine.onWebSocket(
peer._internal.nodeReq,
peer._internal.nodeReq.socket,
peer.websocket
)
},
},
}))
Each layer produces different objects, and Engine.IO needs the raw Node.js ones:
| Layer | Provides |
|---|---|
| Nitro/h3 | H3Event with event.node.req / event.node.res |
| crossws | Peer with peer._internal.nodeReq / peer.websocket |
| Engine.IO | Expects raw IncomingMessage + ServerResponse (HTTP) or IncomingMessage + Socket + WebSocket (WS) |
The bridge just extracts the right objects and passes them through.
HTTP vs WebSocket transports
Socket.IO supports two transports:
- HTTP long-polling - Used as a fallback or during the initial handshake. Goes through h3's
handler(). - WebSocket - The primary transport. Goes through crossws's
websocket.open()hook.
Engine.IO upgrades from long-polling to WebSocket automatically when possible.
Socket.IO and Engine.IO binding
The two are wired together with a single call:
const engine = new Engine()
const io = new Server()
io.bind(engine)
Engine.IO handles the raw connection; Socket.IO handles the event API on top.
Data flow example
What happens when a client calls useRealtimeState('counter', 0) and updates the value:
- The client emits a
storage:setevent with{ key: 'counter', value: 1 } - The message travels over the WebSocket to Nitro
- crossws passes it to the bridge, which hands it to Engine.IO
- Engine.IO passes it up to Socket.IO
- The
storage:sethandler stores the value via Nitro's storage and broadcastsstorage:updatedto thekey:counterroom - Other clients in that room receive the update and their local state refreshes