All skills

WebSocket and realtime communication with sails-hook-sockets for Sails.js applications. Use this skill when building chat, notifications, live updates, presence tracking, or any feature that requires persistent bidirectional communication between server and client.

Use this Skill: https://skilld.dev/gh/sailscastshq/boring-stack/realtime

This session only. Nothing lands on disk.

rulesconfiguration.md

≈2.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Configuration

All WebSocket configuration lives in config/sockets.js. The hook provides sensible defaults — most apps only need to configure security settings for production.

Core Options

// config/sockets.js
module.exports.sockets = {
  // Transport methods (default: ['websocket'])
  // Options: 'websocket', 'polling'
  transports: ['websocket'],

  // Path for Socket.IO connections (default: '/socket.io')
  path: '/socket.io',

  // Milliseconds without a pong before considering connection closed (default: 60000)
  pingTimeout: 60000,

  // Milliseconds between ping packets (default: 25000)
  pingInterval: 25000,

  // Maximum message size in bytes (default: 10E7, ~100MB)
  // Acts as DoS protection — reject oversized messages
  maxHttpBufferSize: 10e7,

  // Whether to include HTTP status code in socket responses (default: true)
  sendStatusCode: true,

  // Whether to include response headers in socket responses (default: true)
  sendResponseHeaders: true
}

CORS and Security

onlyAllowOrigins (Required in Production)

Whitelist of allowed origins for WebSocket connections. This prevents cross-site WebSocket hijacking:

// config/env/production.js
module.exports = {
  sockets: {
    onlyAllowOrigins: ['https://myapp.com', 'https://www.myapp.com']
  }
}

If not set in production, the server will log a warning. Always configure this for production deployments.

In development, CORS is automatically permissive to avoid friction.

Why This Matters

Unlike HTTP CORS (which browsers enforce), WebSocket connections are not subject to same-origin policy by default. Without onlyAllowOrigins, any website could open a WebSocket to your server and piggyback on the user's session cookie — a cross-site WebSocket hijacking attack.

Lifecycle Callbacks

beforeConnect(handshake, cb)

Authorization callback that runs during the WebSocket handshake, before the connection is accepted. Use it to reject unauthorized connections.

// config/sockets.js
module.exports.sockets = {
  beforeConnect: function (handshake, cb) {
    // handshake contains headers, query params, and address info
    const token = handshake.headers['authorization']

    if (!token) {
      return cb(new Error('No authorization token'), false)
    }

    // Verify the token
    try {
      const decoded = sails.helpers.verifyToken(token)
      // You can attach data to the handshake for later use
      handshake.userId = decoded.userId
      return cb(null, true)
    } catch (err) {
      return cb(new Error('Invalid token'), false)
    }
  }
}

Parameters:

Param Type Description
handshake object Handshake/upgrade request with headers, query, address
cb function Callback: cb(err, allow) — pass null, true to accept, or Error, false to reject

afterDisconnect(session, socket, done)

Cleanup callback that runs when a socket disconnects. The session is automatically saved after the callback completes.

// config/sockets.js
module.exports.sockets = {
  afterDisconnect: function (session, socket, done) {
    // Clean up presence tracking
    if (session.userId) {
      User.updateOne({ id: session.userId })
        .set({ isOnline: false, lastSeenAt: Date.now() })
        .then(() => {
          // Notify others that this user went offline
          sails.sockets.broadcast('presence', 'userOffline', {
            userId: session.userId
          })

          done()
        })
        .catch((err) => {
          sails.log.error('Presence cleanup error:', err)
          done()
        })
    } else {
      done()
    }
  }
}

Parameters:

Param Type Description
session object The socket's session data
socket object The disconnected socket object
done function Callback — must be called when cleanup is complete

Session Integration

WebSocket connections share the HTTP session via cookies:

// config/sockets.js
module.exports.sockets = {
  // Expose route for 3rd-party cookie support (default: true)
  // Needed when your API is on a different domain than your frontend
  grant3rdPartyCookie: true
}

Disabling Sessions for Sockets

To connect without session support, send the nosession header during handshake:

// Client-side: connect without session
io.sails.headers = { nosession: true }

This is useful for non-browser clients (React Native, CLI tools) that don't use cookie-based sessions.

Redis Adapter

For multi-server deployments, configure the Redis adapter so all servers share room state and can broadcast to each other.

Setup

npm install @sailshq/socket.io-redis
// config/sockets.js
module.exports.sockets = {
  adapter: '@sailshq/socket.io-redis',
  adapterOptions: {
    host: 'localhost',
    port: 6379,
    pass: '',
    db: 0
  }
}

Production Redis Config

// config/env/production.js
module.exports = {
  sockets: {
    adapter: '@sailshq/socket.io-redis',
    adapterOptions: {
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT) || 6379,
      pass: process.env.REDIS_PASSWORD,
      db: parseInt(process.env.REDIS_DB) || 0
    },
    onlyAllowOrigins: [process.env.APP_URL]
  }
}

Using Redis URL

// config/sockets.js
module.exports.sockets = {
  adapter: '@sailshq/socket.io-redis',
  adapterOptions: {
    url: process.env.REDIS_URL // e.g., 'redis://user:pass@host:6379/0'
  }
}

Using the Official Socket.IO Redis Adapter

// config/sockets.js
module.exports.sockets = {
  adapter: '@socket.io/redis-adapter',
  adapterOptions: {
    host: 'localhost',
    port: 6379
  }
}

Custom Redis Clients

For advanced setups, provide pre-configured Redis clients:

// config/sockets.js
const Redis = require('ioredis')

const pubClient = new Redis({ host: 'redis.example.com', port: 6379 })
const subClient = pubClient.duplicate()

module.exports.sockets = {
  adapter: '@sailshq/socket.io-redis',
  adapterOptions: {
    pubClient,
    subClient
  }
}

Redis Lifecycle Callbacks

Monitor Redis connection health:

// config/sockets.js
module.exports.sockets = {
  adapter: '@sailshq/socket.io-redis',
  adapterOptions: {
    host: 'localhost',
    port: 6379,

    onRedisDisconnect: function () {
      sails.log.warn('Redis adapter disconnected!')
    },

    onRedisReconnect: function () {
      sails.log.info('Redis adapter reconnected')
    }
  }
}

Multi-Server Architecture

When using the Redis adapter, multiple Sails servers share WebSocket state:

Server A (port 3000)          Redis           Server B (port 3001)
───────────────────          ─────           ───────────────────
Socket connects              pub/sub          Socket connects
  ↓                          channel            ↓
sails.sockets.join()  ──→    Sync    ←──  sails.sockets.join()
  ↓                            ↑              ↓
broadcast('room') ────→  Replicated  ──→  Received by sockets
                         to all servers       in same room

How It Works

  1. Room state is shared via the Redis adapter — all servers know which sockets are in which rooms
  2. Admin bus uses a separate Redis pub/sub channel for cross-server operations (join, leave, leaveAll)
  3. Broadcasts automatically reach sockets on all servers
  4. Socket ID operations (join/leave by ID) work cross-server via the admin bus

Admin Bus Configuration

The admin bus runs on a separate Redis connection. You can configure it independently:

// config/sockets.js
module.exports.sockets = {
  adapter: '@sailshq/socket.io-redis',
  adapterOptions: {
    host: 'redis-primary.example.com',
    port: 6379
  },
  adminAdapterOptions: {
    host: 'redis-admin.example.com', // Can use a different Redis instance
    port: 6379
  }
}

Complete Production Configuration

// config/env/production.js
module.exports = {
  sockets: {
    // Security
    onlyAllowOrigins: ['https://myapp.com', 'https://www.myapp.com'],

    // Transport
    transports: ['websocket'],

    // Redis adapter for multi-server
    adapter: '@sailshq/socket.io-redis',
    adapterOptions: {
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT) || 6379,
      pass: process.env.REDIS_PASSWORD || '',
      db: parseInt(process.env.REDIS_DB) || 0,

      onRedisDisconnect: function () {
        sails.log.warn('Socket.IO Redis adapter disconnected')
      },
      onRedisReconnect: function () {
        sails.log.info('Socket.IO Redis adapter reconnected')
      }
    },

    // Authorization
    beforeConnect: function (handshake, cb) {
      // Add your authorization logic here
      return cb(null, true)
    },

    // Cleanup
    afterDisconnect: function (session, socket, done) {
      // Add your cleanup logic here
      done()
    }
  }
}

Source: SKILL.md on GitHub

2 warnings17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides a comprehensive guide and best practices for implementing realtime WebSocket features in Sails.js applications. It covers architectural concepts, room management, model-centric notifications, and client-side integration. The skill emphasizes security by documenting cross-site WebSocket hijacking protections and authorization patterns. No malicious behaviors or security risks were identified.

  • Socket17d

    1 alert: gptAnomaly

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    9/9 files flagged

Signed by skilld at bf19e10. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 8 months ago
Other metadata
metadata
{
  "author": "sailscastshq",
  "version": "1.0.0",
  "tags": "realtime, websockets, sockets, pubsub, socket.io, rooms, sails"
}

README badge

README badge for sailscastshq/boring-stack/realtime