Authentication
Nuxt Realtime exposes the Socket.IO Server instance through the nuxt-realtime:io Nitro hook before any connections are accepted. Register io.use() middleware there to authenticate or authorize clients.
How it works
Create a Nitro server plugin and hook into nuxt-realtime:io. The hook fires after the Server is configured but before it is bound to the transport and before connection handlers are wired up, so every io.use() middleware registered here runs for every incoming connection.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:io', (io) => {
io.use((socket, next) => {
// authenticate here: call next() to allow, next(new Error(...)) to reject
next()
})
})
})
JWT-based authentication
Nuxt Realtime creates the client socket for you, so you can't call io({ auth: ... }) directly. Instead, register a nuxt-realtime:auth app hook. It fires before every connection attempt, including reconnects, so you can supply a fresh token each time instead of baking one in at initial connect:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('nuxt-realtime:auth', (ctx) => {
ctx.auth.token = useAuthToken().value
})
})
For a token that expires and needs refreshing on reconnect, fetch it inside the hook instead of reading a cached value:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('nuxt-realtime:auth', async (ctx) => {
ctx.auth.token = await fetchFreshToken()
})
})
Verify it on the server:
import jwt from 'jsonwebtoken'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:io', (io) => {
io.use((socket, next) => {
const token = socket.handshake.auth?.token
if (!token) return next(new Error('Unauthorized'))
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!)
socket.data.user = payload
next()
}
catch {
next(new Error('Unauthorized'))
}
})
})
})
The middleware rejects unauthenticated connections before a connection event is ever emitted.
Session / cookie-based authentication
Read the session cookie from the underlying HTTP request exposed via socket.request:
import { parse } from 'cookie'
import { unsealData } from 'iron-session'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:io', (io) => {
io.use(async (socket, next) => {
const cookies = parse(socket.request.headers.cookie ?? '')
const sessionCookie = cookies['session']
if (!sessionCookie) return next(new Error('Unauthorized'))
try {
const session = await unsealData(sessionCookie, { password: process.env.SESSION_SECRET! })
socket.data.user = session.user
next()
}
catch {
next(new Error('Unauthorized'))
}
})
})
})
Gating channels with socket.data
event:subscribe and event:publish (used by useRealtimeEvents) are handled internally and always join/broadcast — a handler you register for the same event name runs alongside the built-in one, not instead of it, so it cannot block the join. Gate channel access on your own custom events instead, or restrict who can connect at all via io.use() above.Once authentication sets socket.data.user, use it in your own event handlers to guard access to your own channels:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:io', (io) => {
io.on('connection', (socket) => {
socket.on('room:join', (channel: string) => {
const user = socket.data.user
if (!canAccessChannel(user, channel)) {
socket.emit('error', { message: 'Forbidden' })
return
}
socket.join(`room:${channel}`)
})
})
})
})
socket.data is typed as any by default. You can narrow it by extending Socket.IO's SocketData interface in a type definition file in your project.