WebSocket Integration
WebSocket Integration Prerequisite: The WebSocket gateway must be enabled in Enfyra Admin before connecting. Contact your administrator to enable the gateway for your application. Connection Connect to Enfyra WebSocket using Socket.IO client: import { io } from 'socket.io-client'
WebSocket Integration
Prerequisite: The WebSocket gateway must be enabled in Enfyra Admin before connecting. Contact your administrator to enable the gateway for your application.
Connection
Connect to Enfyra WebSocket using Socket.IO client:
import { io } from 'socket.io-client';
const socket = io(`/${GATEWAY_NAMESPACE}`, {
path: '/socket.io',
withCredentials: true,
});
Third app URL format: connect to the Socket.IO namespace on the third app origin and use the local Socket.IO transport path.
GATEWAY_NAMESPACE- configured backend gateway path without the leading slash in the string template (for gateway metadata path/chat, usechat)path- local Socket.IO transport path. The third app proxies/socket.io/**to the Enfyra app bridge/ws/socket.io/**.
Direct Enfyra app clients may use the built-in bridge form io('/ws/chat', { path: '/ws/socket.io' }). Third apps should prefer io('/chat', { path: '/socket.io' }) so the namespace remains /chat while the transport is proxied through the third app.
Connection Options
| Option | Type | Default | Description |
|---|---|---|---|
transports |
string[] | ['polling', 'websocket'] |
Transport methods |
path |
string | /socket.io |
Socket.IO transport endpoint on the app origin |
withCredentials |
boolean | true |
Send cookies with requests |
reconnect |
string | 'true' |
Auto reconnect when backend disconnects |
Connection Events
socket.on('connect', () => {
console.log('Connected');
});
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error.message);
});
Relay Events
| Event | Description |
|---|---|
backend_disconnected |
Backend disconnected, waiting to reconnect |
backend_reconnected |
Backend reconnected |
socket.on('backend_disconnected', () => {
console.log('Backend disconnected, waiting...');
});
socket.on('backend_reconnected', () => {
console.log('Backend reconnected');
});
Sending Messages
socket.emit('message', {
text: 'Hello!',
roomId: '123',
});
Recommended: ACK + async result events (better UX)
Enfyra runs WebSocket event handlers asynchronously (queued). For the best developer experience:
- Use Socket.IO ack callback to confirm the event is queued and receive a
requestId. - Listen for
ws:result/ws:errorto get the handler outcome (including script logs).
socket.emit('message', { text: 'Hello!', roomId: '123' }, (ack) => {
// ack = { queued: boolean, requestId: string, eventName: string, error?: { code, message } }
console.log('queued?', ack.queued, 'requestId', ack.requestId);
});
socket.on('ws:result', (payload) => {
// payload = { requestId, eventName, success: true, result: any, logs: any[] }
console.log('ws result', payload);
});
socket.on('ws:error', (payload) => {
// payload = { requestId, eventName, success: false, code, message, logs?: any[], details?: any }
console.error('ws error', payload);
});
Common Events
| Event | Description | Payload |
|---|---|---|
message |
Send chat message | { text: string, roomId: string } |
typing |
User typing indicator | { roomId: string, isTyping: boolean } |
joinRoom |
Join a room | { roomId: string } |
leaveRoom |
Leave a room | { roomId: string } |
updateStatus |
Update user status | { status: string } |
Receiving Messages
socket.on('newMessage', (data) => {
console.log('New message:', data);
});
Common Server Events
| Event | Description | Payload |
|---|---|---|
connected |
Connection acknowledged | { message: string } |
newMessage |
New chat message | { id: string, text: string, userId: string } |
userJoined |
User joined room | { userId: string, name: string } |
userLeft |
User left room | { userId: string } |
notification |
Push notification | { type: string, payload: any } |
ws:result |
Handler completed | { requestId, eventName, success: true, result, logs } |
ws:error |
Handler error / queue error | { requestId, eventName, success: false, code, message, logs?, details? } |
Remove Listener
// Remove specific listener
socket.off('message', handler);
// Remove all listeners for an event
socket.off('message');
Message Format
Client Server
socket.emit('message', {
text: 'Hello World!',
roomId: 'room-123'
});
Server Client
{
"event": "newMessage",
"data": {
"id": "abc",
"text": "Hello World!",
"userId": "user123"
}
}
Error Handling
Connection Errors
socket.on('connect_error', (error) => {
console.error('Connection error:', error.message);
});
Disconnection
socket.on('disconnect', (reason) => {
// Reasons:
// - "io server disconnect" - Server disconnected
// - "io client disconnect" - Client disconnected
// - "ping timeout"
// - "transport close"
// - "transport error"
});
Reconnection
const socket = io(url, {
path: '/socket.io',
withCredentials: true,
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
socket.on('reconnect_failed', () => {
console.error('Reconnection failed');
});
Server-Side Handler Scripts (@SOCKET API)
Handler scripts run in an isolated sandbox. Use the @SOCKET template macro (preferred over $ctx.$socket).
Available Methods
WS context (connection handler + event handler)
| Method | Description | Available in |
|---|---|---|
@SOCKET.reply(event, data) |
Send to the triggering client only | connection + event |
@SOCKET.join(room) |
Join a room in the current namespace | connection + event |
@SOCKET.leave(room) |
Leave a room | connection + event |
@SOCKET.emitToUser(userId, event, data) |
Send to a specific user across all gateways | connection + event |
@SOCKET.emitToRoom(room, event, data) |
Send to a named room across all gateways | connection + event |
@SOCKET.emitToGateway(path, event, data) |
Broadcast to all connections on a namespace | connection + event |
@SOCKET.broadcast(event, data) |
Broadcast to all connections on all gateways | connection + event |
@SOCKET.disconnect() |
Force-disconnect the current socket from the gateway | connection handler only |
HTTP context (handler / hook)
Only emitToUser, emitToRoom, emitToGateway, and broadcast are available (no socket to reply/join/leave/disconnect).
Handler Script Examples
Join room (event: joinRoom):
const { roomId } = @BODY;
if (!roomId) @THROW.badRequest('roomId is required');
@SOCKET.join(`chat_${roomId}`);
@SOCKET.emitToRoom(`chat_${roomId}`, 'userJoined', { userId: @USER.id });
return { joined: roomId };
Send message (event: message):
const { text, roomId } = @BODY;
const msg = await #message.create({
data: { text, roomId, senderId: @USER.id }
});
@SOCKET.emitToRoom(`chat_${roomId}`, 'newMessage', msg);
return { sent: true };
Push notification to a user (event: notify):
const { targetUserId, message } = @BODY;
@SOCKET.emitToUser(targetUserId, 'notification', {
from: @USER.id,
message,
timestamp: Date.now(),
});
return { notified: true };
Leave room (event: leaveRoom):
const { roomId } = @BODY;
@SOCKET.leave(`chat_${roomId}`);
@SOCKET.emitToRoom(`chat_${roomId}`, 'userLeft', { userId: @USER.id });
return { left: roomId };
Kick user / reject connection (event: kickSelf or connection handler):
// Connection handler: reject if user is banned
const bannedResult = await #banned_user.find({
filter: { userId: { _eq: @USER.id } },
limit: 1,
});
if (bannedResult.data.length > 0) {
@SOCKET.reply('kicked', { reason: 'You are banned' });
@SOCKET.disconnect();
return;
}
// Event handler: notify a user that their account is suspended
// Note: @SOCKET.disconnect() is NOT available in event handlers — only in connection handlers.
const userResult = await #user_definition.find({
filter: { id: { _eq: @USER.id } },
fields: 'id,isSuspended',
limit: 1,
});
const user = userResult.data[0];
if (user?.isSuspended) {
@SOCKET.reply('suspended', { reason: 'Account suspended' });
return;
}
Connection handler (connectionHandlerScript):
@SOCKET.reply('connected', { message: 'Welcome!', userId: @USER.id });
@LOGS('user connected', @USER.id);
From HTTP handler/hook (e.g., order status update):
const order = await #order.update({
filter: { id: { _eq: @PARAMS.id } },
body: { status: 'shipped' }
});
@SOCKET.emitToUser(order.userId, 'orderUpdate', { orderId: order.id, status: 'shipped' });
return order;
Examples
Chat Application
import { io, Socket } from 'socket.io-client';
class ChatService {
private socket: Socket;
connect() {
this.socket = io('/chat', {
path: '/socket.io',
withCredentials: true,
});
this.socket.on('newMessage', (message) => {
this.onNewMessage(message);
});
this.socket.on('userJoined', (data) => {
this.onUserJoined(data);
});
}
sendMessage(text: string, roomId: string) {
this.socket.emit('message', { text, roomId });
}
joinRoom(roomId: string) {
this.socket.emit('joinRoom', { roomId });
}
disconnect() {
this.socket?.disconnect();
}
private onNewMessage(message: any) {
console.log('New message:', message);
}
private onUserJoined(data: any) {
console.log('User joined:', data);
}
}
Testing WebSocket handlers (recommended)
When developing WebSocket handler scripts, you can test them without building a client app.
POST /admin/test/run (kind: websocket_event)
Send a test payload and a handler script; the server runs the script with a simulated @SOCKET and returns:
- result: the handler return value
- logs: script logs (@LOGS(...))
- emitted: array of { method, args } — each @SOCKET call captured (e.g. { method: 'reply', args: ['reply', { ok: true }] })
curl -X POST "$API_URL/admin/test/run" \
-H "content-type: application/json" \
-d '{
"kind": "websocket_event",
"gatewayPath": "/chat",
"eventName": "message",
"timeoutMs": 5000,
"payload": { "text": "hello" },
"script": " @LOGS(\"received\", @BODY); @SOCKET.reply(\"reply\", { ok: true }); return { ok: true }; "
}'
Vue 3 Composition API
import { ref, onUnmounted } from 'vue';
import { io, Socket } from 'socket.io-client';
export function useWebSocket(gatewayPath: string) {
const socket = ref<Socket | null>(null);
const isConnected = ref(false);
const connect = () => {
socket.value = io(`/${gatewayPath}`, {
path: '/socket.io',
withCredentials: true,
});
socket.value.on('connect', () => {
isConnected.value = true;
});
socket.value.on('disconnect', () => {
isConnected.value = false;
});
};
const disconnect = () => {
socket.value?.disconnect();
socket.value = null;
isConnected.value = false;
};
const emit = (event: string, data: any) => {
socket.value?.emit(event, data);
};
const on = (event: string, callback: (data: any) => void) => {
socket.value?.on(event, callback);
};
const off = (event: string, callback?: (data: any) => void) => {
socket.value?.off(event, callback as any);
};
onUnmounted(() => {
disconnect();
});
return { socket, isConnected, connect, disconnect, emit, on, off };
}
// Usage
const { isConnected, connect, emit, on } = useWebSocket('chat');
connect();
on('newMessage', (message) => {
messages.value.push(message);
});
emit('message', { text: 'Hello!', roomId: '123' });
React Hook
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useWebSocket(gatewayPath: string) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
socketRef.current = io(`/${gatewayPath}`, {
path: '/socket.io',
withCredentials: true,
});
socketRef.current.on('connect', () => setIsConnected(true));
socketRef.current.on('disconnect', () => setIsConnected(false));
return () => {
socketRef.current?.disconnect();
};
}, [gatewayPath]);
const emit = (event: string, data: any) => {
socketRef.current?.emit(event, data);
};
const on = (event: string, callback: (data: any) => void) => {
socketRef.current?.on(event, callback);
};
const off = (event: string, callback?: (data: any) => void) => {
socketRef.current?.off(event, callback as any);
};
return { isConnected, emit, on, off };
}
// Usage
function ChatComponent() {
const { isConnected, emit, on, off } = useWebSocket('chat');
useEffect(() => {
const handleNewMessage = (message: any) => {
setMessages(prev => [...prev, message]);
};
on('newMessage', handleNewMessage);
return () => {
off('newMessage', handleNewMessage);
};
}, [on, off]);
return <div>{isConnected ? 'Connected' : 'Disconnected'}</div>;
}
Best Practices
Clean Up Listeners
onUnmounted(() => {
socket.off('newMessage');
socket.disconnect();
});
Validate Messages
socket.on('newMessage', (data) => {
if (!data.id || !data.text) {
console.warn('Invalid message format');
return;
}
addMessage(data);
});
Debounce Rapid Events
import { debounce } from 'lodash';
const emitTyping = debounce((roomId: string) => {
socket.emit('typing', { roomId, isTyping: true });
}, 300);
Troubleshooting
| Issue | Solution |
|---|---|
| Connection fails | Check URL format, CORS settings |
| Messages not received | Verify event name matches server, listener registered before message arrives |
| Frequent disconnections | Check network stability, server load, increase ping timeout |