Realtime

Custom WebSocket Handlers

Build custom WebSocket message handlers, push messages to connected clients, and react to WebSocket events using hook extensions.

The other pages in this section cover client-side WebSocket usage - authenticating, subscribing to changes, and performing CRUD actions. This page covers the server side: using hook extensions to react to connections, handle custom message types, and push data to clients.

See the hooks reference for a full list of available WebSocket events.

Before You Start

You will need:

React to Connection Events

Use action hooks to run logic when clients connect, disconnect, or authenticate. The callback receives a client object representing the WebSocket connection.

The client object has the following key properties:

PropertyDescription
uidUnique identifier for the connection.
send(data)Send a string message to the client.
accountabilityObject containing user and role IDs after authentication.
export default ({ action }) => {
    action('websocket.connect', ({ client }) => {
        console.log(`Client connected: ${client.uid}`);
    });

    action('websocket.close', ({ client }) => {
        console.log(`Client disconnected: ${client.uid}`);
    });

    action('websocket.auth.success', ({ client }) => {
        console.log(`Authenticated: user ${client.accountability?.user}`);
    });
};

The websocket.auth.failure and websocket.error action events are also available for handling failed authentication attempts and connection errors.

Handle Custom Message Types

Use an action on websocket.message to detect custom message types sent by clients. The message parameter is already parsed from JSON - you do not need to call JSON.parse().

export default ({ action }) => {
    action('websocket.message', ({ message, client }) => {
        if (message.type !== 'typing-indicator') return;

        console.log(`User ${client.accountability?.user} is typing in ${message.channel}`);
    });
};

This action runs after Directus has processed the message. To modify or block messages before processing, use a filter instead.

Intercept and Modify Messages

Use a filter on websocket.message to transform messages before Directus processes them. Filters receive the parsed message as the payload and must return it for processing to continue. To block a message entirely, throw an error.

export default ({ filter }) => {
    filter('websocket.message', (message) => {
        if (message.type === 'subscribe' && !message.query?.fields) {
            message.query = { ...message.query, fields: ['id', 'date_created'] };
        }

        return message;
    });
};
Filters are blocking - slow filter logic will delay message processing for the client. Keep filters fast and avoid expensive operations like database queries where possible.

Push Messages to Clients

The WebSocketService is available through the hook's context and provides access to all connected clients. Use clients() to get the full set, or broadcast() to send a message to all (or a filtered subset of) clients.

export default ({ action }, { services }) => {
    action('items.create', ({ payload, collection }) => {
        if (collection !== 'notifications') return;

        const wsService = new services.WebSocketService();

        wsService.broadcast(JSON.stringify({ type: 'notification', data: payload }));
    });
};

The broadcast method accepts an optional second argument to target specific users or roles:

wsService.broadcast(
    JSON.stringify({ type: 'notification', data: payload }),
    { user: '1a2b3c4d-...' }
);

If you need more control over which clients receive the message, use clients() to get the Set of connected clients and iterate over them directly:

const wsService = new services.WebSocketService();

for (const client of wsService.clients()) {
    if (client.accountability?.role === 'admin-role-id') {
        client.send(JSON.stringify({ type: 'alert', data: payload }));
    }
}
WebSocketService only provides access to REST WebSocket clients. GraphQL subscription and logs streaming clients are managed by separate controllers and are not accessible through this service.

Next Steps

Get once-a-month release notes & real‑world code tips...no fluff. 🐰