Socket.IO Integration
This guide describes how to use the real-time functionality in BAASIX using Socket.IO integration.
Table of Contents
- Overview
- Configuration
- WAL-Based Realtime (PostgreSQL)
- Available Events
- Client Integration
- Authentication
- Custom Rooms
- Custom Event Handlers
- Client Examples
Overview
BAASIX provides real-time functionality through Socket.IO integration powered by PostgreSQL's Write-Ahead Log (WAL) logical replication. This approach captures database changes at the database level, ensuring reliable and efficient real-time updates even when changes are made directly to the database.
Key Features
- WAL-Based: Uses PostgreSQL logical replication for reliable change capture
- Per-Collection Control: Enable/disable realtime per collection via schema settings
- Action Filtering: Choose which actions (insert/update/delete) to broadcast
- Custom Rooms: Create custom rooms for game lobbies, chat rooms, or any on-demand grouping
- Custom Handlers: Register server-side handlers for custom socket events (perfect for extensions)
Configuration
Environment Variables
# Enable Socket.IO functionality
SOCKET_ENABLED=true
# Optional: Configure Socket.IO path (default: /socket)
SOCKET_PATH=/realtime
# Optional: CORS origins for Socket.IO (comma-separated)
SOCKET_CORS_ENABLED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
# Redis configuration for Socket.IO clustering (optional)
SOCKET_REDIS_ENABLED=true
SOCKET_REDIS_URL=redis://localhost:6379
SOCKET_REDIS_KEY=socket.ioRedis Adapter for Scaling
BAASIX automatically configures Redis adapter for Socket.IO when enabled, allowing horizontal scaling across multiple server instances.
Multi-Instance WAL Consumption
PostgreSQL WAL replication slots only allow one consumer at a time. When running multiple instances:
- With Redis (
SOCKET_REDIS_ENABLED=true): BAASIX uses Redis-based leader election. Only one instance consumes WAL changes and broadcasts to all instances via Redis pub/sub. - Without Redis: Single instance mode - the instance directly consumes WAL changes.
# Multi-instance configuration
SOCKET_ENABLED=true
SOCKET_REDIS_ENABLED=true
SOCKET_REDIS_URL=redis://localhost:6379
SOCKET_REDIS_KEY=socket.ioThe leader election ensures:
- Only one instance connects to the WAL replication slot
- Automatic failover if the leader instance goes down
- Lock renewal every 10 seconds to maintain leadership
- Clean handoff when shutting down gracefully
WAL-Based Realtime (PostgreSQL)
BAASIX uses PostgreSQL's logical replication to capture database changes in real-time. This is more reliable than hook-based approaches because:
- Captures all changes - Including direct SQL updates, triggers, and other database-level modifications
- Transactional - Only broadcasts committed changes
- Efficient - Minimal overhead on your application
Enabling WAL Logical Replication
To use WAL-based realtime, your PostgreSQL server must have logical replication enabled:
1. Check Current Configuration
SHOW wal_level;
-- Should return 'logical'2. Enable Logical Replication
Edit your postgresql.conf:
# Required for WAL-based realtime
wal_level = logical
# Recommended settings
max_replication_slots = 10
max_wal_senders = 10Restart PostgreSQL after making changes.
3. For Cloud Providers
- AWS RDS/Aurora: Set
rds.logical_replicationto 1 in parameter group - Google Cloud SQL: Enable
cloudsql.logical_decodingflag - Azure Database: Set
azure.replication_supporttological - Supabase/Neon: Already enabled by default
Enabling Realtime on Collections
You can enable realtime via the Admin UI or API:
Via Admin UI
- Go to Schema Management
- Click on a collection's settings (gear icon)
- Enable Realtime toggle
- Select which actions to broadcast (Insert, Update, Delete)
- Save changes
Via API
# Enable realtime for a collection
POST /realtime/collections/:collection/enable
{
"actions": ["insert", "update", "delete"],
"replicaIdentityFull": true # Optional: Include old values on update/delete
}
# Disable realtime for a collection
POST /realtime/collections/:collection/disable
# Check realtime status
GET /realtime/status
# Get enabled collections
GET /realtime/collectionsVia Schema Definition
The realtime config is stored in the schema definition:
{
"name": "products",
"realtime": {
"enabled": true,
"actions": ["insert", "update", "delete"]
},
"fields": {
// ... fields
}
}Checking Replication Status
GET /realtime/configReturns:
{
"data": {
"walLevel": "logical",
"maxReplicationSlots": 10,
"maxWalSenders": 10,
"replicationSlotExists": true,
"publicationExists": true,
"tablesInPublication": ["baasix_Product", "baasix_Order"]
}
}Available Events
Collection Events (WAL-Based)
When realtime is enabled on a collection, events are emitted in the format {collection}:{action}:
products:insert- New record insertedproducts:update- Record updatedproducts:delete- Record deleted
Event payload structure:
{
action: "insert", // or "update", "delete"
collection: "products",
data: {
id: "uuid",
name: "Product Name",
price: 99.99,
// ... all fields
},
timestamp: "2026-01-09T10:30:00.000Z"
}System Events
connected- Emitted when authenticated and connectedworkflow:execution:update- Workflow execution progressworkflow:execution:complete- Workflow execution finished
Custom Room Events
room:user:joined- User joined a custom roomroom:user:left- User left a custom roomroom:{event}- Custom room messages
Client Integration
Connecting to Socket
import { io } from 'socket.io-client';
const socket = io('http://your-api-domain', {
path: '/realtime',
auth: { token: 'your-jwt-token' },
transports: ['websocket', 'polling'],
});
socket.on('connect', () => {
console.log('Connected to BAASIX realtime');
});
socket.on('connected', (data) => {
console.log('Authenticated as:', data.userId);
});Subscribing to Collections
// Subscribe to a collection
socket.emit('subscribe', { collection: 'products' }, (response) => {
if (response.status === 'success') {
console.log('Subscribed to products');
}
});
// Listen for changes
socket.on('products:insert', (data) => {
console.log('New product:', data.data);
});
socket.on('products:update', (data) => {
console.log('Product updated:', data.data);
});
socket.on('products:delete', (data) => {
console.log('Product deleted:', data.data);
});
// Unsubscribe when done
socket.emit('unsubscribe', { collection: 'products' });Authentication
Socket connections require a valid JWT token:
const token = localStorage.getItem('auth_token');
const socket = io('http://your-api-domain', {
path: '/realtime',
auth: { token },
});Custom Rooms
BAASIX supports custom rooms for use cases like game lobbies, chat rooms, or collaborative editing. These are separate from collection subscriptions and can be created on-demand.
Joining a Room
socket.emit('room:join', { room: 'game:lobby' }, (response) => {
if (response.status === 'success') {
console.log('Joined room:', response.room);
}
});Leaving a Room
socket.emit('room:leave', { room: 'game:lobby' }, (response) => {
console.log('Left room');
});Listening for Room Events
// User joined the room
socket.on('room:user:joined', (data) => {
console.log(`User ${data.userId} joined ${data.room}`);
});
// User left the room
socket.on('room:user:left', (data) => {
console.log(`User ${data.userId} left ${data.room}`);
});
// Custom room messages (prefixed with room:)
socket.on('room:chat', (data) => {
console.log(`${data.sender.userId}: ${data.payload.message}`);
});
socket.on('room:game:state', (data) => {
console.log('Game state update:', data.payload);
});Sending Messages to a Room
// Send a message to all room members
socket.emit(
'room:message',
{
room: 'game:lobby',
event: 'chat',
payload: { message: 'Hello everyone!' },
},
(response) => {
console.log('Message sent');
},
);Room Example: Chat Room
// Join chat room
socket.emit('room:join', { room: 'chat:general' });
// Send chat message
function sendMessage(text) {
socket.emit('room:message', {
room: 'chat:general',
event: 'message',
payload: { text, timestamp: Date.now() },
});
}
// Receive messages
socket.on('room:message', (data) => {
addMessageToUI(data.sender.userId, data.payload.text);
});Room Example: Game Lobby
// Join game lobby
socket.emit('room:join', { room: `game:${gameId}` });
// Send player move
socket.emit('room:message', {
room: `game:${gameId}`,
event: 'move',
payload: { x: 10, y: 20, action: 'attack' },
});
// Receive game events
socket.on('room:move', (data) => {
handlePlayerMove(data.sender.userId, data.payload);
});
socket.on('room:game:over', (data) => {
showGameOver(data.payload.winner);
});Custom Event Handlers
For extensions or advanced use cases, you can register custom event handlers on the server that clients can invoke:
Client Side
// Trigger a custom handler
socket.emit(
'custom',
{
event: 'game:roll-dice',
payload: { sides: 6 },
},
(response) => {
if (response.status === 'success') {
console.log('Dice result:', response.result);
}
},
);Server Side (Extensions)
// In your extension
import { SocketService } from '@baasix/baasix';
// Register a custom handler
SocketService.registerMessageHandler('game:roll-dice', async (socket, data, callback) => {
const sides = data.sides || 6;
const result = Math.floor(Math.random() * sides) + 1;
callback?.({ status: 'success', result });
// Optionally broadcast to a room
// SocketService.broadcastToRoom('game:123', 'dice:rolled', { player: socket.userId, result });
});
// Register a room validator (optional access control)
SocketService.registerRoomValidator('vip:', async (socket, roomName) => {
// Only VIP users can join VIP rooms
return socket.userRole.name === 'vip' || socket.userRole.name === 'administrator';
});Server-Side Broadcasting
import { SocketService } from '@baasix/baasix';
// Broadcast to a specific room
SocketService.broadcastToRoom('game:123', 'game:state', { phase: 'battle' });
// Broadcast to all connected users
SocketService.broadcastToAll('system:announcement', { message: 'Maintenance in 5 minutes' });
// Send to a specific user
SocketService.sendToUser(userId, 'private:notification', { text: 'You have a new message' });
// Room utilities
SocketService.getRoomMembers('game:123'); // string[] of socket IDs
SocketService.getRoomMemberCount('game:123'); // number
SocketService.roomExists('game:123'); // boolean
SocketService.getCustomRooms(); // Map<string, number>Client Examples
React Hook
import { useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';
function useRealtime(collection) {
const [socket, setSocket] = useState(null);
const [data, setData] = useState([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
const token = localStorage.getItem('auth_token');
const newSocket = io('http://your-api-domain', {
path: '/realtime',
auth: { token },
});
newSocket.on('connect', () => setConnected(true));
newSocket.on('disconnect', () => setConnected(false));
// Subscribe to collection
newSocket.emit('subscribe', { collection });
// Listen for changes
newSocket.on(`${collection}:insert`, (payload) => {
setData((prev) => [...prev, payload.data]);
});
newSocket.on(`${collection}:update`, (payload) => {
setData((prev) => prev.map((item) => (item.id === payload.data.id ? payload.data : item)));
});
newSocket.on(`${collection}:delete`, (payload) => {
setData((prev) => prev.filter((item) => item.id !== payload.data.id));
});
setSocket(newSocket);
return () => {
newSocket.emit('unsubscribe', { collection });
newSocket.disconnect();
};
}, [collection]);
return { data, setData, connected, socket };
}
// Usage
function ProductList() {
const { data: products, connected } = useRealtime('products');
return (
<div>
<span>{connected ? '🟢 Live' : '🔴 Offline'}</span>
{products.map((p) => (
<div key={p.id}>{p.name}</div>
))}
</div>
);
}Vue Composable
import { ref, onMounted, onUnmounted } from 'vue';
import { io } from 'socket.io-client';
export function useRealtime(collection) {
const socket = ref(null);
const data = ref([]);
const connected = ref(false);
onMounted(() => {
const token = localStorage.getItem('auth_token');
socket.value = io('http://your-api-domain', {
path: '/realtime',
auth: { token },
});
socket.value.on('connect', () => (connected.value = true));
socket.value.on('disconnect', () => (connected.value = false));
socket.value.emit('subscribe', { collection });
socket.value.on(`${collection}:insert`, (payload) => {
data.value.push(payload.data);
});
socket.value.on(`${collection}:update`, (payload) => {
const idx = data.value.findIndex((i) => i.id === payload.data.id);
if (idx >= 0) data.value[idx] = payload.data;
});
socket.value.on(`${collection}:delete`, (payload) => {
data.value = data.value.filter((i) => i.id !== payload.data.id);
});
});
onUnmounted(() => {
if (socket.value) {
socket.value.emit('unsubscribe', { collection });
socket.value.disconnect();
}
});
return { data, connected, socket };
}Related Documentation
- Schema Reference - Collection schema configuration
- SDK Guide - Using the BAASIX JavaScript/TypeScript SDK
- Hooks and Endpoints - Server-side customization
- API Routes Reference - Complete API documentation