
A plugin for Strapi CMS that provides the ability for Socket IO integration
released April 28, 2026
npm install @strapi-community/plugin-ioReal-time WebSocket integration for Strapi v5 with Socket.IO
Add real-time capabilities to your Strapi application with WebSocket support. Automatically broadcast content changes, manage user connections, and build live features like chat, notifications, and collaborative editing.
npm install @strapi-community/plugin-ioCreate or update config/plugins.js:
module.exports = {
io: {
enabled: true,
config: {
contentTypes: ['api::article.article'],
socket: {
serverOptions: {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST']
}
}
}
}
}
};npm run developimport { io } from 'socket.io-client';
const socket = io('http://localhost:1337');
socket.on('article:create', (article) => {
console.log('New article published:', article);
});
socket.on('article:update', (article) => {
console.log('Article updated:', article);
});That's it! Your application now has real-time updates.
# Using npm
npm install @strapi-community/plugin-io
# Using yarn
yarn add @strapi-community/plugin-io
# Using pnpm
pnpm add @strapi-community/plugin-ioThe simplest setup to get started:
// config/plugins.js
module.exports = {
io: {
enabled: true,
config: {
// Monitor these content types for changes
contentTypes: [
'api::article.article',
'api::comment.comment'
]
}
}
};Fine-tune the plugin behavior:
// config/plugins.js
module.exports = {
io: {
enabled: true,
config: {
// Content types with specific actions and populate
contentTypes: [
{
uid: 'api::article.article',
actions: ['create', 'update'], // Only these events
populate: '*' // Include all relations
},
{
uid: 'api::comment.comment',
actions: ['create', 'delete'],
populate: ['author', 'article'] // Only specific relations
},
{
uid: 'api::order.order',
populate: { // Strapi populate syntax
customer: { fields: ['name', 'email'] },
items: { populate: ['product'] }
}
}
],
// Socket.IO server configuration
socket: {
serverOptions: {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
methods: ['GET', 'POST'],
credentials: true
},
pingTimeout: 60000,
pingInterval: 25000
}
},
// Custom event handlers
events: [
{
name: 'connection',
handler: ({ strapi, io }, socket) => {
strapi.log.info(`Client connected: ${socket.id}`);
}
},
{
name: 'disconnect',
handler: ({ strapi, io }, socket) => {
strapi.log.info(`Client disconnected: ${socket.id}`);
}
}
],
// Initialization hook
hooks: {
init: ({ strapi, $io }) => {
strapi.log.info('[Socket.IO] Server initialized');
}
}
}
}
};Include relations in emitted events by adding the populate option to content types. When configured, the plugin refetches entities with populated relations after create/update operations.
| Format | Example | Description |
|---|---|---|
'*' or true | populate: '*' | Include all relations (1 level deep) |
| String array | populate: ['author', 'category'] | Only specific relations |
| Object | populate: { author: { fields: ['name'] } } | Full Strapi populate syntax |
contentTypes: [
// All relations with wildcard
{ uid: 'api::article.article', populate: '*' },
// Specific relations only
{
uid: 'api::comment.comment',
populate: ['author', 'post']
},
// Strapi populate syntax with field selection
{
uid: 'api::order.order',
populate: {
customer: { fields: ['username', 'email'] },
items: {
populate: { product: { fields: ['name', 'price'] } }
}
}
},
// No populate (only base fields)
{ uid: 'api::log.log' } // populate not set = no relations
]Note: The
populateoption only affectscreateandupdateevents. Delete events always contain minimal data (id, documentId) since the entity no longer exists.
The plugin automatically removes sensitive fields from all emitted data for security. This works in addition to Strapi's built-in private: true field filtering.
The following fields are always removed from emitted data:
password, salt, hashresetPasswordToken, confirmationTokenrefreshToken, accessToken, tokensecret, apiKey, api_keyprivateKey, private_keyAdd your own sensitive fields to the block list:
// config/plugins.js
module.exports = {
io: {
enabled: true,
config: {
contentTypes: ['api::user.user'],
// Additional fields to never emit
sensitiveFields: [
'creditCard',
'ssn',
'socialSecurityNumber',
'bankAccount',
'internalNotes'
]
}
}
};sanitize.contentAPI removes private: true fieldsemit() and raw() methods are protected// Even with populate, sensitive fields are stripped
contentTypes: [
{
uid: 'api::user.user',
populate: '*' // Relations included, but passwords etc. still removed
}
]Security Note: Fields are matched case-insensitively and also partial matches work (e.g.,
apiKeyblocksmyApiKey,user_api_key, etc.)
Recommended environment-based configuration:
// config/plugins.js
module.exports = ({ env }) => ({
io: {
enabled: env.bool('SOCKET_IO_ENABLED', true),
config: {
contentTypes: env.json('SOCKET_IO_CONTENT_TYPES', []),
socket: {
serverOptions: {
cors: {
origin: env('CLIENT_URL', 'http://localhost:3000')
}
}
}
}
}
});# .env
SOCKET_IO_ENABLED=true
CLIENT_URL=https://your-app.com
SOCKET_IO_CONTENT_TYPES=["api::article.article","api::comment.comment"]Access the Socket.IO instance anywhere in your Strapi application:
// In a controller, service, or lifecycle
const io = strapi.$io;
// Emit to all connected clients
strapi.$io.raw({
event: 'notification',
data: {
message: 'Server maintenance in 5 minutes',
type: 'warning'
}
});
// Send private message to a specific socket
strapi.$io.sendPrivateMessage(socketId, 'order:updated', {
orderId: 123,
status: 'shipped'
});
// Emit to all clients in a room
strapi.$io.server.to('admin-room').emit('dashboard:update', {
activeUsers: 42,
revenue: 15000
});
// Emit to a specific namespace
strapi.$io.emitToNamespace('admin', 'alert', {
message: 'New user registered'
});import { io } from 'socket.io-client';
const socket = io('http://localhost:1337');
socket.on('connect', () => {
console.log('Connected to server');
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
});
// Listen for content type events
socket.on('article:create', (data) => {
console.log('New article:', data);
});
socket.on('article:update', (data) => {
console.log('Article updated:', data);
});
socket.on('article:delete', (data) => {
console.log('Article deleted:', data.documentId);
});import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';
function ArticlesList() {
const [articles, setArticles] = useState([]);
useEffect(() => {
const socket = io('http://localhost:1337');
// Listen for new articles
socket.on('article:create', (article) => {
setArticles(prev => [article, ...prev]);
});
// Listen for updates
socket.on('article:update', (article) => {
setArticles(prev =>
prev.map(a => a.documentId === article.documentId ? article : a)
);
});
// Listen for deletions
socket.on('article:delete', (data) => {
setArticles(prev =>
prev.filter(a => a.documentId !== data.documentId)
);
});
return () => socket.disconnect();
}, []);
return (
<div>
{articles.map(article => (
<div key={article.documentId}>{article.title}</div>
))}
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { io } from 'socket.io-client';
const articles = ref([]);
let socket;
onMounted(() => {
socket = io('http://localhost:1337');
socket.on('article:create', (article) => {
articles.value = [article, ...articles.value];
});
socket.on('article:update', (article) => {
const index = articles.value.findIndex(a => a.documentId === article.documentId);
if (index !== -1) {
articles.value[index] = article;
}
});
socket.on('article:delete', (data) => {
articles.value = articles.value.filter(a => a.documentId !== data.documentId);
});
});
onUnmounted(() => {
socket?.disconnect();
});
</script>
<template>
<div v-for="article in articles" :key="article.documentId">
{{ article.title }}
</div>
</template>Subscribe to updates for specific entities only:
// Client-side: Subscribe to a specific article
socket.emit('subscribe-entity', {
uid: 'api::article.article',
id: 123
}, (response) => {
if (response.success) {
console.log('Subscribed to article 123');
}
});
// Now you only receive updates for article 123
socket.on('article:update', (data) => {
console.log('Article 123 was updated:', data);
});
// Unsubscribe when done
socket.emit('unsubscribe-entity', {
uid: 'api::article.article',
id: 123
});Benefits:
Organize connections into rooms:
// Server-side: Add socket to a room
strapi.$io.joinRoom(socketId, 'premium-users');
// Get all sockets in a room
const sockets = await strapi.$io.getSocketsInRoom('premium-users');
console.log(`${sockets.length} premium users online`);
// Broadcast to a specific room
strapi.$io.server.to('premium-users').emit('exclusive-offer', {
discount: 20,
expiresIn: '24h'
});
// Remove socket from a room
strapi.$io.leaveRoom(socketId, 'premium-users');
// Disconnect a specific socket
strapi.$io.disconnectSocket(socketId, 'Kicked by admin');The plugin provides 12 utility functions available on strapi.$io:
joinRoom(socketId, roomName)Add a socket to a room.
const success = strapi.$io.joinRoom(socketId, 'premium-users');
// Returns: true if socket found and joined, false otherwiseleaveRoom(socketId, roomName)Remove a socket from a room.
const success = strapi.$io.leaveRoom(socketId, 'premium-users');
// Returns: true if socket found and left, false otherwisegetSocketsInRoom(roomName)Get all sockets in a specific room.
const sockets = await strapi.$io.getSocketsInRoom('premium-users');
// Returns: [{ id: 'socket-id', user: { username: 'john' } }, ...]sendPrivateMessage(socketId, event, data)Send a message to a specific socket.
strapi.$io.sendPrivateMessage(socketId, 'order:shipped', {
orderId: 123,
trackingNumber: 'ABC123'
});broadcast(socketId, event, data)Broadcast from a socket to all other connected clients.
strapi.$io.broadcast(socketId, 'user:typing', {
username: 'john',
channel: 'general'
});emitToNamespace(namespace, event, data)Emit an event to all clients in a namespace.
strapi.$io.emitToNamespace('admin', 'alert', {
message: 'New user registered',
level: 'info'
});disconnectSocket(socketId, reason)Force disconnect a specific socket.
const success = strapi.$io.disconnectSocket(socketId, 'Session expired');
// Returns: true if socket found and disconnected, false otherwisesubscribeToEntity(socketId, uid, id)Subscribe a socket to a specific entity (server-side).
const result = await strapi.$io.subscribeToEntity(socketId, 'api::article.article', 123);
// Returns: { success: true, room: 'api::article.article:123', uid, id }
// or: { success: false, error: 'Socket not found' }unsubscribeFromEntity(socketId, uid, id)Unsubscribe a socket from a specific entity.
const result = strapi.$io.unsubscribeFromEntity(socketId, 'api::article.article', 123);
// Returns: { success: true, room: 'api::article.article:123', uid, id }getEntitySubscriptions(socketId)Get all entity subscriptions for a socket.
const result = strapi.$io.getEntitySubscriptions(socketId);
// Returns: {
// success: true,
// subscriptions: [
// { uid: 'api::article.article', id: '123', room: 'api::article.article:123' }
// ]
// }emitToEntity(uid, id, event, data)Emit an event to all clients subscribed to a specific entity.
strapi.$io.emitToEntity('api::article.article', 123, 'article:commented', {
commentId: 456,
author: 'jane'
});getEntityRoomSockets(uid, id)Get all sockets subscribed to a specific entity.
const sockets = await strapi.$io.getEntityRoomSockets('api::article.article', 123);
// Returns: [{ id: 'socket-id', user: { username: 'john' } }, ...]The plugin supports multiple authentication strategies.
const socket = io('http://localhost:1337');
// No auth - placed in 'Public' role room// 1. Get JWT token from login
const response = await fetch('http://localhost:1337/api/auth/local', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identifier: 'user@example.com',
password: 'password123'
})
});
const { jwt } = await response.json();
// 2. Connect with JWT
const socket = io('http://localhost:1337', {
auth: {
strategy: 'jwt',
token: jwt
}
});
// User is placed in their role room (e.g., 'Authenticated')// 1. Create API token in Strapi Admin:
// Settings > Global Settings > API Tokens > Create new token
// 2. Connect with API token
const socket = io('http://localhost:1337', {
auth: {
strategy: 'api-token',
token: 'your-api-token-here'
}
});Events are automatically filtered based on the user's role:
// Authenticated user with 'Editor' role
socket.on('article:create', (data) => {
// Only receives events for content types they have permission to access
});Configure permissions in the Strapi admin panel:
The plugin implements multiple security layers to protect your real-time connections.
For admin panel connections (Live Presence), the plugin uses secure session tokens:
+------------------+ +------------------+ +------------------+
| Admin Browser | ---> | Session Endpoint| ---> | Socket.IO |
| (Strapi Admin) | | /io/presence/ | | Server |
+------------------+ +------------------+ +------------------+
| | |
| 1. Request session | |
| (Admin JWT in header) | |
+------------------------>| |
| | |
| 2. Return session token | |
| (UUID, 10 min TTL) | |
|<------------------------+ |
| | |
| 3. Connect Socket.IO | |
| (Session token in auth) | |
+-------------------------------------------------->|
| | |
| 4. Validate & connect | |
|<--------------------------------------------------+Security Features:
Prevent abuse with configurable rate limits:
// In config/plugins.js
module.exports = {
io: {
enabled: true,
config: {
security: {
rateLimit: {
enabled: true,
maxEventsPerSecond: 10, // Max events per socket per second
maxConnectionsPerIp: 50 // Max connections from single IP
}
}
}
}
};Restrict access by IP address:
// In config/plugins.js
module.exports = {
io: {
enabled: true,
config: {
security: {
ipWhitelist: ['192.168.1.0/24', '10.0.0.1'], // Only these IPs allowed
ipBlacklist: ['203.0.113.50'], // These IPs blocked
requireAuthentication: true // Require JWT/API token
}
}
}
};Monitor active sessions via admin API:
# Get session statistics
GET /io/security/sessions
Authorization: Bearer <admin-jwt>
# Response:
{
"data": {
"activeSessions": 5,
"expiringSoon": 1,
"activeSocketConnections": 3,
"sessionTTL": 600000,
"refreshCooldown": 30000
}
}
# Force logout a user (invalidate all their sessions)
POST /io/security/invalidate/:userId
Authorization: Bearer <admin-jwt>
# Response:
{
"data": {
"userId": 1,
"invalidatedSessions": 2,
"message": "Successfully invalidated 2 session(s)"
}
}The plugin provides a full admin interface for configuration and monitoring.
Requires Strapi v5.13+
After installation, live statistics widgets appear on your Strapi admin homepage:

Shows:
Updates automatically every 5 seconds.

Shows:
Navigate to Settings > Socket.IO > Settings for visual configuration:
Path: /admin/settings/io/settings

includeRelations)Navigate to Settings > Socket.IO > Monitoring for live statistics:
Path: /admin/settings/io/monitoring

When editing content in the Content Manager, a Live Presence panel appears in the sidebar showing:

How It Works:
Access monitoring data programmatically via the monitoring service:
const monitoringService = strapi.plugin('io').service('monitoring');getConnectionStats()Get current connection statistics.
const stats = monitoringService.getConnectionStats();
// Returns:
// {
// connected: 42,
// rooms: [
// { name: 'Authenticated', members: 35, isEntityRoom: false },
// { name: 'api::article.article:123', members: 5, isEntityRoom: true }
// ],
// sockets: [
// {
// id: 'abc123',
// connected: true,
// rooms: ['Authenticated'],
// entitySubscriptions: [{ uid: 'api::article.article', id: '123', room: '...' }],
// handshake: { address: '::1', time: '...', query: {} },
// user: { id: 1, username: 'john' }
// }
// ],
// entitySubscriptions: {
// total: 15,
// byContentType: { 'api::article.article': 10, 'api::comment.comment': 5 },
// rooms: ['api::article.article:123', ...]
// }
// }getEventStats()Get event statistics.
const stats = monitoringService.getEventStats();
// Returns:
// {
// totalEvents: 1234,
// eventsByType: { 'create': 500, 'update': 600, 'delete': 134 },
// lastReset: 1703123456789,
// eventsPerSecond: '2.50'
// }getEventLog(limit)Get recent event log entries.
const logs = monitoringService.getEventLog(50);
// Returns:
// [
// { timestamp: 1703123456789, type: 'create', data: {...} },
// { timestamp: 1703123456790, type: 'connect', data: {...} }
// ]logEvent(type, data)Manually log an event.
monitoringService.logEvent('custom-event', {
action: 'user-action',
userId: 123
});resetStats()Reset all statistics and clear event log.
monitoringService.resetStats();sendTestEvent(eventName, data)Send a test event to all connected clients.
const result = monitoringService.sendTestEvent('test', { message: 'Hello!' });
// Returns:
// {
// success: true,
// eventName: 'test',
// data: { message: 'Hello!', timestamp: 1703123456789, test: true },
// recipients: 42
// }Custom Analytics Dashboard:
// In a custom controller
async getAnalytics(ctx) {
const monitoring = strapi.plugin('io').service('monitoring');
ctx.body = {
connections: monitoring.getConnectionStats(),
events: monitoring.getEventStats(),
recentActivity: monitoring.getEventLog(10)
};
}Health Check Endpoint:
async healthCheck(ctx) {
const monitoring = strapi.plugin('io').service('monitoring');
const stats = monitoring.getConnectionStats();
ctx.body = {
status: 'healthy',
websocket: {
connected: stats.connected,
rooms: stats.rooms.length
}
};
}Full TypeScript definitions are included for excellent IDE support.
import type {
SocketIO,
SocketIOConfig,
EmitOptions,
RawEmitOptions,
PluginSettings,
MonitoringService,
SettingsService,
ConnectionStats,
EventStats
} from '@strapi-community/plugin-io/types';// config/plugins.ts
import type { SocketIOConfig } from '@strapi-community/plugin-io/types';
export default {
io: {
enabled: true,
config: {
contentTypes: [
{
uid: 'api::article.article',
actions: ['create', 'update', 'delete']
}
],
socket: {
serverOptions: {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
methods: ['GET', 'POST']
}
}
}
} satisfies SocketIOConfig
}
};// In your Strapi code
import type { SocketIO } from '@strapi-community/plugin-io/types';
// Type-safe access
const io: SocketIO = strapi.$io;
// All methods have full IntelliSense
await io.emit({
event: 'create',
schema: strapi.contentTypes['api::article.article'],
data: { title: 'New Article' }
});
// Helper functions are typed
const sockets = await io.getSocketsInRoom('premium-users');
io.sendPrivateMessage(sockets[0].id, 'welcome', { message: 'Hello!' });The plugin is optimized for production environments.
Role and permission data is cached for 5 minutes, reducing database queries by up to 90%.
Bulk operations are automatically debounced to prevent event flooding during data imports.
All event emissions are processed in parallel for maximum throughput.
Efficient connection management with automatic cleanup of stale connections.
// config/plugins.js (production)
module.exports = ({ env }) => ({
io: {
enabled: true,
config: {
contentTypes: env.json('SOCKET_IO_CONTENT_TYPES'),
socket: {
serverOptions: {
cors: {
origin: env('CLIENT_URL'),
credentials: true
},
// Optimize for production
pingTimeout: 60000,
pingInterval: 25000,
maxHttpBufferSize: 1e6,
transports: ['websocket', 'polling']
}
},
// Use Redis for horizontal scaling
hooks: {
init: async ({ strapi, $io }) => {
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: env('REDIS_URL') });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
$io.server.adapter(createAdapter(pubClient, subClient));
strapi.log.info('[Socket.IO] Redis adapter connected');
}
}
}
}
});Good news: The API is 100% compatible! Most projects migrate in under 1 hour.
Update Strapi to v5
npm install @strapi/strapi@5 @strapi/plugin-users-permissions@5Update the plugin
npm uninstall strapi-plugin-io
npm install @strapi-community/plugin-io@latestTest your application
npm run developYour configuration stays the same - no code changes needed!
strapi-plugin-io -> @strapi-community/plugin-ioFor detailed migration instructions, see docs/guide/migration.md.
Build complete real-time applications with these complementary Strapi v5 plugins:
Enterprise email management with OAuth 2.0 support. Perfect for sending transactional emails triggered by Socket.IO events.
Use case: Send email notifications when real-time events occur.
Advanced session tracking and monitoring. Track Socket.IO connections, monitor active users, and analyze session patterns.
Use case: Monitor who's connected to your WebSocket server in real-time.
Bookmark management system with real-time sync. Share bookmarks instantly with your team using Socket.IO integration.
Use case: Collaborative bookmark management with live updates.
We welcome contributions! Here's how you can help:
Found a bug? Open an issue with:
Have an idea? Start a discussion to:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)# Clone the repository
git clone https://github.com/strapi-community/strapi-plugin-io.git
cd strapi-plugin-io
# Install dependencies
npm install
# Build the plugin
npm run build
# Run in watch mode
npm run watch
# Verify structure
npm run verifyCopyright (c) 2024 Strapi Community
Original Authors:
Enhanced and Maintained by:
Maintained until: December 2026
@strapi-community/plugin-ioFor full changelog, see CHANGELOG.md.
Documentation | API Reference | Examples | GitHub
Made with love for the Strapi community
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
