Controlling Real-Time Event Broadcasts in FeathersJS: Filtering Socket.io Events by User Context
Learn how to control who receives real-time events in FeathersJS by implementing event filtering at the service or application level, preventing unauthorized WebSocket broadcasts.
17 Mar 2026, 07:36 UTC

The Problem: Broadcasting to Everyone, Including Users Who Shouldn't See It
When building a FeathersJS service with real-time capabilities, the default behavior is to broadcast events like created, updated, patched, and removed to all connected WebSocket clients. This works fine for public data, but becomes problematic when you need to restrict who sees what—imagine a support ticket system where agents should see new tickets, but regular users should only see their own.
The challenge isn't just preventing unauthorized HTTP API access; it's stopping the real-time event stream from leaking sensitive information to clients that shouldn't receive it.
Thesis: Event Filtering at the Service Level
FeathersJS doesn't provide built-in per-event filtering for socket.io broadcasts, but you can intercept and control event emission by overriding service methods or using a custom app-level event filter. The most maintainable approach is to create a wrapper that conditionally emits events based on user context or data ownership.
Approach 1: Override Service Methods with Conditional Emission
Instead of relying on Feathers' automatic event broadcasting, you can manually emit events only when specific conditions are met. Here's how to implement this for a tickets service where only the ticket owner and assigned agents should receive updates:
const errors = require('feathers-errors');
class TicketService extends Service {
async create(data, params) {
const result = await super.create(data, params);
// Only broadcast to relevant parties
const { user } = params;
const relevantUsers = [result.userId, result.assignedTo];
if (relevantUsers.includes(user.id)) {
this.emit('created', result, params);
}
return result;
}
async patch(id, data, params) {
const result = await super.patch(id, data, params);
const { user } = params;
const isOwner = result.userId === user.id;
const isAssignedAgent = result.assignedTo === user.id;
if (isOwner || isAssignedAgent) {
this.emit('patched', result, params);
}
return result;
}
}
This approach gives you fine-grained control but requires overriding each method you want to filter. It also means you're bypassing Feathers' automatic event system, which can lead to inconsistencies if you forget to emit an event.
Approach 2: App-Level Event Filter Middleware
A more robust solution is to intercept all events at the application level and apply filtering logic before broadcasting. This works by hooking into the socket.io connection layer and examining each event before it's sent:
// In your app configuration file
const socketio = require('feathers-socketio');
app.configure(socketio({
// Default options
}, (io) => {
io.on('connection', (socket) => {
socket.on('join', (data) => {
socket.join(data.room);
});
});
}));
// Custom event filter
app.service('tickets').hooks({
after: {
create: filterEvent('created'),
patch: filterEvent('patched'),
update: filterEvent('updated'),
remove: filterEvent('removed')
}
});
function filterEvent(eventName) {
return async context => {
const { app, result, params } = context;
const { user } = params;
// Emit to specific rooms or users instead of all
if (eventName === 'created') {
// Emit to the user who created it and any relevant parties
app.channel(`user-${user.id}`).send(eventName, result);
// Emit to specific user's private channel
if (result.assignedTo) {
app.channel(`user-${result.assignedTo}`).send(eventName, result);
}
}
return context;
};
}
Working Example: Role-Based Event Broadcasting
Here's a complete implementation for a notification service where admins see all notifications, but regular users only see their own:
const { Service } = require('feathers-memory');
class NotificationService extends Service {
async create(data, params) {
const result = await super.create(data, params);
// Always emit to the creator
this.emit('created', result, params);
// Emit to admins only
if (params.user && params.user.role === 'admin') {
this.emit('admin-notification', result, params);
}
return result;
}
}
// Hook to filter outgoing events
const filterNotifications = context => {
const { event, result, params } = context;
const { user } = params;
if (event === 'created') {
// Non-admins only see their own notifications
if (user.role !== 'admin' && result.userId !== user.id) {
return false; // Block the event
}
}
return context;
};
// Apply to socket.io
app.service('notifications').hooks({
after: {
create: filterNotifications
}
});
Trade-offs and Limitations
Memory Overhead: Maintaining user-specific channels or rooms increases memory usage, especially with many concurrent connections. Each user's private channel requires tracking their socket IDs.
Complexity vs. Simplicity: The manual emission approach is simpler to understand but harder to maintain. The channel-based approach is more scalable but requires additional setup and understanding of Feathers' channel system.
CORS and Client Compatibility: Socket.io event filtering doesn't affect the underlying HTTP API. You must still implement proper authentication and authorization at the service method level. Additionally, older socket.io client versions may not support room-based broadcasting efficiently.
Verification and Next Steps
To verify your event filtering is working correctly:
- Test with multiple clients: Connect two different authenticated clients and perform actions that should or shouldn't trigger events for each.
- Monitor network traffic: Use browser dev tools to inspect WebSocket messages and confirm unauthorized users aren't receiving filtered events.
- Check server logs: Add logging to see which events are being emitted and to whom.
- Load test: Simulate many concurrent connections to ensure your filtering logic doesn't become a bottleneck.
For production use, consider implementing rate limiting on real-time events and monitoring connection counts to prevent event storms from overwhelming your server.