Skip to content

Socket.IO Integration

KickJS ships with a ws-based WebSocket adapter (@forinda/kickjs-ws), but you can integrate Socket.IO for features like automatic reconnection, rooms, acknowledgements, and binary support.

Setup

bash
pnpm add socket.io

Create a Socket.IO Adapter

ts
// src/adapters/socketio.adapter.ts
import { Server, type Socket } from 'socket.io'
import { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'

const log = Logger.for('SocketIOAdapter')

export interface SocketIOAdapterOptions {
  /** CORS configuration */
  cors?: {
    origin: string | string[]
    methods?: string[]
    credentials?: boolean
  }
  /** Path for the Socket.IO endpoint (default: '/socket.io') */
  path?: string
  /** Custom namespaces to register */
  namespaces?: SocketIONamespace[]
}

export interface SocketIONamespace {
  /** Namespace path (e.g. '/chat', '/notifications') */
  namespace: string
  /** Handler setup function — receives the namespace and DI container */
  setup: (nsp: any, container: Container) => void
}

export class SocketIOAdapter implements AppAdapter {
  name = 'SocketIOAdapter'
  private io: Server | null = null

  constructor(private options: SocketIOAdapterOptions = {}) {}

  afterStart(server: any, container: Container): void {
    this.io = new Server(server, {
      cors: this.options.cors ?? { origin: '*' },
      path: this.options.path ?? '/socket.io',
    })

    // Default namespace
    this.io.on('connection', (socket: Socket) => {
      log.info(`Connected: ${socket.id}`)

      socket.on('disconnect', (reason) => {
        log.info(`Disconnected: ${socket.id} (${reason})`)
      })
    })

    // Custom namespaces
    for (const ns of this.options.namespaces ?? []) {
      const nsp = this.io.of(ns.namespace)
      ns.setup(nsp, container)
      log.info(`Namespace registered: ${ns.namespace}`)
    }

    // Register io instance in DI for injection
    container.registerInstance(SOCKET_IO, this.io)

    log.info(`Socket.IO listening at ${this.options.path ?? '/socket.io'}`)
  }

  async shutdown(): Promise<void> {
    if (this.io) {
      await new Promise<void>((resolve) => this.io!.close(() => resolve()))
      log.info('Socket.IO server closed')
    }
  }
}

/** DI token for injecting the Socket.IO server */
export const SOCKET_IO = Symbol('SocketIO')

Register in Bootstrap

ts
import { bootstrap } from '@forinda/kickjs-http'
import { SocketIOAdapter } from './adapters/socketio.adapter'
import { modules } from './modules'

bootstrap({
  modules,
  adapters: [
    new SocketIOAdapter({
      cors: { origin: 'http://localhost:5173', credentials: true },
      namespaces: [
        {
          namespace: '/chat',
          setup: (nsp, container) => {
            nsp.on('connection', (socket) => {
              console.log(`Chat connected: ${socket.id}`)

              socket.on('message', (data) => {
                // Broadcast to room or all
                nsp.emit('message', {
                  from: socket.id,
                  ...data,
                  timestamp: new Date().toISOString(),
                })
              })

              socket.on('join-room', (room) => {
                socket.join(room)
                socket.to(room).emit('user-joined', { userId: socket.id })
              })

              socket.on('leave-room', (room) => {
                socket.leave(room)
                socket.to(room).emit('user-left', { userId: socket.id })
              })
            })
          },
        },
        {
          namespace: '/notifications',
          setup: (nsp, container) => {
            nsp.on('connection', (socket) => {
              // Join user-specific room for targeted notifications
              const userId = socket.handshake.auth?.userId
              if (userId) socket.join(`user:${userId}`)
            })
          },
        },
      ],
    }),
  ],
})

Inject Socket.IO in Services

Use the SOCKET_IO token to inject the io server anywhere:

ts
import { Service, Inject } from '@forinda/kickjs-core'
import { SOCKET_IO } from '../adapters/socketio.adapter'
import type { Server } from 'socket.io'

@Service()
export class NotificationPushService {
  constructor(@Inject(SOCKET_IO) private io: Server) {}

  /** Send a notification to a specific user */
  notifyUser(userId: string, event: string, data: any) {
    this.io.of('/notifications').to(`user:${userId}`).emit(event, data)
  }

  /** Broadcast to all connected clients */
  broadcast(event: string, data: any) {
    this.io.emit(event, data)
  }

  /** Send to a specific room */
  toRoom(room: string, event: string, data: any) {
    this.io.to(room).emit(event, data)
  }
}

Client-Side

ts
import { io } from 'socket.io-client'

// Connect to default namespace
const socket = io('http://localhost:3000')

// Connect to a specific namespace
const chat = io('http://localhost:3000/chat')
const notifications = io('http://localhost:3000/notifications', {
  auth: { userId: 'user-123' },
})

// Listen for events
chat.on('message', (msg) => console.log('New message:', msg))
notifications.on('alert', (alert) => console.log('Alert:', alert))

// Send events
chat.emit('message', { text: 'Hello everyone!' })
chat.emit('join-room', 'general')

Socket.IO vs ws

@forinda/kickjs-wsSocket.IO
ProtocolRaw WebSocketCustom protocol over WebSocket/polling
ReconnectionManualAutomatic
RoomsVia RoomManagerBuilt-in
AcknowledgementsManualBuilt-in callbacks
BinaryManualAutomatic
FallbackWebSocket onlyLong-polling fallback
Bundle size~50KB~300KB (client + server)
Decorators@WsController, @OnMessageUse adapter pattern above
Best forLightweight, low-level controlFull-featured real-time apps

With Authentication

ts
// Middleware for Socket.IO authentication
io.use((socket, next) => {
  const token = socket.handshake.auth?.token
  if (!token) return next(new Error('Authentication required'))

  try {
    const user = jwt.verify(token, JWT_SECRET)
    socket.data.user = user
    next()
  } catch {
    next(new Error('Invalid token'))
  }
})

// Access user in handlers
io.on('connection', (socket) => {
  console.log(`Authenticated user: ${socket.data.user.email}`)
})

With KickJS Auth

If you're using @forinda/kickjs-auth, you can reuse your JWT strategy:

ts
import { JwtStrategy } from '@forinda/kickjs-auth'

const jwtStrategy = new JwtStrategy({ secret: JWT_SECRET })

io.use(async (socket, next) => {
  // Create a mock request object for the strategy
  const mockReq = {
    headers: { authorization: `Bearer ${socket.handshake.auth?.token}` },
  }
  const user = await jwtStrategy.validate(mockReq)
  if (!user) return next(new Error('Unauthorized'))
  socket.data.user = user
  next()
})

Released under the MIT License.