Learn how to use lifecycle hooks for initialization, adapters, and custom middleware.
Hooks are similar to Strapi model lifecycles. They are functions called at specific points in the Socket.IO plugin lifecycle.
The init hook is triggered immediately after the Socket.IO server instance is constructed. It's perfect for:
- Adding server adapters (Redis, MongoDB)
- Registering middleware
- Configuring namespaces
- Setting up custom logic
init(context: {
strapi: Strapi;
io: SocketIOInstance;
}): void | Promise<void>Scale your Socket.IO server across multiple instances using Redis:
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
module.exports = ({ env }) => ({
io: {
enabled: true,
config: {
contentTypes: ['api::article.article'],
hooks: {
async init({ strapi, io }) {
const pubClient = createClient({
url: env('REDIS_URL', 'redis://localhost:6379')
});
const subClient = pubClient.duplicate();
await Promise.all([
pubClient.connect(),
subClient.connect()
]);
io.server.adapter(createAdapter(pubClient, subClient));
strapi.log.info('[io] ✅ Redis adapter initialized');
// Handle errors
pubClient.on('error', (err) => {
strapi.log.error('[io] Redis pub client error:', err);
});
subClient.on('error', (err) => {
strapi.log.error('[io] Redis sub client error:', err);
});
}
}
}
}
});Benefits:
- ✅ Horizontal scaling across multiple servers
- ✅ Load balancing
- ✅ Session persistence
- ✅ High availability
Use MongoDB as the adapter for distributed Socket.IO:
const { MongoClient } = require('mongodb');
const { createAdapter } = require('@socket.io/mongo-adapter');
module.exports = ({ env }) => ({
io: {
enabled: true,
config: {
hooks: {
async init({ strapi, io }) {
const mongoClient = new MongoClient(env('MONGO_URL'));
await mongoClient.connect();
const mongoCollection = mongoClient
.db('mydb')
.collection('socket.io-adapter-events');
io.server.adapter(createAdapter(mongoCollection, {
addCreatedAtField: true
}));
strapi.log.info('[io] ✅ MongoDB adapter initialized');
}
}
}
}
});Add authentication, logging, or rate limiting middleware:
module.exports = {
io: {
enabled: true,
config: {
hooks: {
init({ strapi, io }) {
io.server.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication required'));
}
// Verify JWT token
const user = await strapi.plugins['users-permissions']
.services.jwt.verify(token);
if (!user) {
return next(new Error('Invalid token'));
}
// Attach user to socket
socket.data.user = user;
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
strapi.log.info('[io] Authentication middleware registered');
}
}
}
}
};hooks: {
init({ strapi, io }) {
io.server.use((socket, next) => {
strapi.log.info('[io] Connection attempt:', {
socketId: socket.id,
ip: socket.handshake.address,
userAgent: socket.handshake.headers['user-agent'],
timestamp: new Date().toISOString()
});
// Add connection timestamp
socket.data.connectedAt = Date.now();
next();
});
}
}hooks: {
init({ strapi, io }) {
const allowedIPs = [
'127.0.0.1',
'::1',
'192.168.1.0/24'
];
io.server.use((socket, next) => {
const clientIP = socket.handshake.address;
if (!isIPAllowed(clientIP, allowedIPs)) {
strapi.log.warn(`[io] Blocked connection from ${clientIP}`);
return next(new Error('IP not whitelisted'));
}
next();
});
}
}const rateLimit = require('socket.io-rate-limit');
hooks: {
init({ strapi, io }) {
io.server.use(rateLimit({
tokensPerInterval: 100,
interval: 60000, // 100 requests per minute
fireImmediately: true,
onLimitReached: (socket) => {
strapi.log.warn(`[io] Rate limit exceeded: ${socket.id}`);
socket.emit('error', {
message: 'Too many requests. Please slow down.'
});
}
}));
strapi.log.info('[io] Rate limiting enabled');
}
}Create separate communication channels:
hooks: {
init({ strapi, io }) {
// Admin namespace
const adminNamespace = io.server.of('/admin');
adminNamespace.use(async (socket, next) => {
// Admin-only authentication
const token = socket.handshake.auth.token;
const user = await verifyAdminToken(token);
if (!user || user.role.type !== 'admin') {
return next(new Error('Admin access required'));
}
socket.data.user = user;
next();
});
adminNamespace.on('connection', (socket) => {
strapi.log.info(`[io] Admin connected: ${socket.data.user.username}`);
socket.on('broadcast', (data) => {
// Broadcast to all users
io.server.emit('admin:announcement', data);
});
});
// Chat namespace
const chatNamespace = io.server.of('/chat');
chatNamespace.on('connection', (socket) => {
strapi.log.info('[io] Chat user connected');
socket.on('message', (data) => {
chatNamespace.emit('message', data);
});
});
strapi.log.info('[io] Namespaces configured: /admin, /chat');
}
}Client usage:
// Connect to admin namespace
const adminSocket = io('http://localhost:1337/admin', {
auth: { token: adminToken }
});
// Connect to chat namespace
const chatSocket = io('http://localhost:1337/chat', {
auth: { token: userToken }
});hooks: {
init({ strapi, io }) {
const connections = new Map();
io.server.on('connection', (socket) => {
const connectionInfo = {
socketId: socket.id,
ip: socket.handshake.address,
userAgent: socket.handshake.headers['user-agent'],
connectedAt: new Date(),
userId: socket.data.user?.id
};
connections.set(socket.id, connectionInfo);
// Log to database
strapi.entityService.create('api::connection-log.connection-log', {
data: connectionInfo
});
socket.on('disconnect', () => {
const info = connections.get(socket.id);
const duration = Date.now() - info.connectedAt.getTime();
strapi.log.info(`[io] Session ended: ${socket.id} (${duration}ms)`);
// Update log
strapi.entityService.update('api::connection-log.connection-log', {
where: { socketId: socket.id },
data: {
disconnectedAt: new Date(),
duration
}
});
connections.delete(socket.id);
});
});
// Periodic stats logging
setInterval(() => {
strapi.log.info(`[io] Active connections: ${connections.size}`);
}, 60000); // Every minute
}
}hooks: {
init({ strapi, io }) {
const eventStats = new Map();
// Intercept all events
io.server.use((socket, next) => {
socket.use((packet, next) => {
const [eventName] = packet;
// Track event
const count = eventStats.get(eventName) || 0;
eventStats.set(eventName, count + 1);
next();
});
next();
});
// Report stats every 5 minutes
setInterval(() => {
const stats = Array.from(eventStats.entries())
.map(([event, count]) => ({ event, count }))
.sort((a, b) => b.count - a.count);
strapi.log.info('[io] Event statistics:', stats);
// Reset stats
eventStats.clear();
}, 300000);
}
}Automatically join users to rooms based on their role or properties:
hooks: {
init({ strapi, io }) {
io.server.on('connection', async (socket) => {
if (!socket.data.user) return;
const user = socket.data.user;
// Join user-specific room
socket.join(`user-${user.id}`);
// Join role-based room
socket.join(`role-${user.role.type}`);
// Join premium room
if (user.isPremium) {
socket.join('premium');
}
// Join location-based rooms
if (user.country) {
socket.join(`country-${user.country}`);
}
strapi.log.info(`[io] User ${user.username} joined rooms:`,
Array.from(socket.rooms)
);
});
}
}Handle server shutdown gracefully:
hooks: {
init({ strapi, io }) {
const shutdown = async (signal) => {
strapi.log.info(`[io] Received ${signal}, closing connections...`);
// Notify all clients
io.server.emit('server:shutdown', {
message: 'Server is restarting',
reconnectIn: 5000
});
// Wait for messages to be sent
await new Promise(resolve => setTimeout(resolve, 1000));
// Close all connections
const sockets = await io.server.fetchSockets();
sockets.forEach(socket => {
socket.disconnect(true);
});
strapi.log.info(`[io] Closed ${sockets.length} connections`);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
}Implement complex room joining logic:
hooks: {
init({ strapi, io }) {
io.server.on('connection', (socket) => {
socket.on('join-channel', async ({ channelId, password }) => {
try {
// Fetch channel
const channel = await strapi.entityService.findOne(
'api::channel.channel',
channelId
);
if (!channel) {
socket.emit('error', { message: 'Channel not found' });
return;
}
// Check password if private
if (channel.isPrivate && channel.password !== password) {
socket.emit('error', { message: 'Invalid password' });
return;
}
// Check member limit
const roomSize = (await io.server.in(`channel-${channelId}`).fetchSockets()).length;
if (channel.maxMembers && roomSize >= channel.maxMembers) {
socket.emit('error', { message: 'Channel is full' });
return;
}
// Join room
socket.join(`channel-${channelId}`);
// Notify others
socket.to(`channel-${channelId}`).emit('user:joined', {
userId: socket.data.user?.id,
username: socket.data.user?.username
});
socket.emit('channel:joined', {
channelId,
members: roomSize + 1
});
} catch (error) {
strapi.log.error('[io] Join channel error:', error);
socket.emit('error', { message: 'Failed to join channel' });
}
});
});
}
}Add custom helper functions to the IO instance:
hooks: {
init({ strapi, io }) {
// Add custom helper
io.kickUser = async (userId, reason) => {
const stats = strapi.plugin('io').service('monitoring').getConnectionStats();
const userSocket = stats.sockets.find(s => s.user?.id === userId);
if (userSocket) {
io.server.to(userSocket.id).emit('kicked', { reason });
io.disconnectSocket(userSocket.id, reason);
return true;
}
return false;
};
// Add broadcast to role
io.broadcastToRole = (roleName, event, data) => {
io.server.to(`role-${roleName}`).emit(event, data);
};
strapi.log.info('[io] Custom helpers registered');
}
}Usage:
// Kick abusive user
await strapi.$io.kickUser(123, 'Violation of terms');
// Broadcast to all admins
strapi.$io.broadcastToRole('admin', 'system:alert', {
level: 'critical',
message: 'Database backup required'
});// test/hooks.test.js
describe('Socket.IO Hooks', () => {
test('should have Redis adapter', async () => {
const adapter = strapi.$io.server.adapter;
expect(adapter.constructor.name).toBe('RedisAdapter');
});
test('should have custom helpers', () => {
expect(typeof strapi.$io.kickUser).toBe('function');
expect(typeof strapi.$io.broadcastToRole).toBe('function');
});
test('should auto-join user room', (done) => {
const socket = io('http://localhost:1337', {
auth: { token: userToken }
});
socket.on('connect', () => {
// Verify user joined their room
socket.emit('get-rooms', (rooms) => {
expect(rooms).toContain(`user-${userId}`);
done();
});
});
});
});- Use adapters for production scaling
- Implement rate limiting
- Log important events
- Handle errors gracefully
- Clean up resources on disconnect
- Use TypeScript for complex logic
- Block the event loop
- Store large objects in socket.data
- Forget to handle adapter errors
- Skip authentication middleware
- Leave connections open unnecessarily
- Events - Custom event handlers
- Content Types - Automatic content events
- API Reference - Core API methods
- Socket.IO Adapters - Official adapter docs