BaasixBaasix

Socket.IO Integration

← Back to Documentation Home

This guide describes how to use the real-time functionality in BAASIX using Socket.IO integration.

Table of Contents

  1. Overview
  2. Configuration
  3. WAL-Based Realtime (PostgreSQL)
  4. Available Events
  5. Client Integration
  6. Authentication
  7. Custom Rooms
  8. Custom Event Handlers
  9. 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.io

Redis 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.io

The 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:

  1. Captures all changes - Including direct SQL updates, triggers, and other database-level modifications
  2. Transactional - Only broadcasts committed changes
  3. 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 = 10

Restart PostgreSQL after making changes.

3. For Cloud Providers

  • AWS RDS/Aurora: Set rds.logical_replication to 1 in parameter group
  • Google Cloud SQL: Enable cloudsql.logical_decoding flag
  • Azure Database: Set azure.replication_support to logical
  • Supabase/Neon: Already enabled by default

Enabling Realtime on Collections

You can enable realtime via the Admin UI or API:

Via Admin UI

  1. Go to Schema Management
  2. Click on a collection's settings (gear icon)
  3. Enable Realtime toggle
  4. Select which actions to broadcast (Insert, Update, Delete)
  5. 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/collections

Via 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/config

Returns:

{
  "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 inserted
  • products:update - Record updated
  • products: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 connected
  • workflow:execution:update - Workflow execution progress
  • workflow:execution:complete - Workflow execution finished

Custom Room Events

  • room:user:joined - User joined a custom room
  • room:user:left - User left a custom room
  • room:{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 };
}

On this page