Skip to main content

HyperHQ Plugin Socket.IO Connection Guide

This guide explains how to connect your plugin to HyperHQ using Socket.IO for real-time, bidirectional communication.

Table of Contents

Overview

HyperHQ provides a Socket.IO server that enables real-time communication between the frontend and plugins. This allows for:

  • Real-time Updates: Instant notifications and data synchronization
  • Bidirectional Communication: Both HyperHQ and plugins can initiate communication
  • Event-Driven Architecture: React to system events as they happen
  • Progress Reporting: Live progress updates for long-running operations
  • Persistent Connection: Maintains connection with automatic reconnection

Connection Architecture

Default Configuration

  • Server URL: http://localhost:52789
  • Namespace: /plugin
  • Transport: WebSocket (with polling fallback)
  • Default Port: 52789 (configurable in HyperHQ settings)

Getting Started

Step 1: Define Socket.IO Configuration in Manifest

Add Socket.IO configuration to your plugin.json:

{
"id": "your-plugin-id",
"name": "Your Plugin",
"version": "1.0.0",
"executable": "plugin.exe",

"socketio": {
"enabled": true,
"namespace": "/plugin",
"reconnect": true,
"reconnectAttempts": 10,
"reconnectDelay": 2000,
"timeout": 5000
},

"permissions": {
"networkAccess": true
}
}

Step 2: Install Socket.IO Client Library

Choose the appropriate client library for your language:

JavaScript/Node.js

npm install socket.io-client

Python

pip install python-socketio

C#/.NET

<PackageReference Include="SocketIOClient" Version="3.0.8" />

Go

go get github.com/googollee/go-socket.io

Step 3: Understand Authentication

HyperHQ uses a challenge-response authentication model for plugin security:

  1. HyperHQ launches your plugin and passes an authentication challenge via environment variable
  2. Your plugin connects to Socket.IO and sends the challenge back
  3. HyperHQ validates the challenge and returns a session token
  4. Your plugin uses the session token for all subsequent requests

Environment Variables

When HyperHQ launches your plugin, it provides these environment variables:

VariableDescriptionExample
HYPERHQ_PLUGIN_IDYour plugin's unique ID"my-plugin"
HYPERHQ_SOCKET_PORTSocket.IO server port"52789"
HYPERHQ_AUTH_CHALLENGEOne-time authentication challenge"abc123..."
HYPERHQ_PLUGIN_NAMEYour plugin's name"My Plugin"
HYPERHQ_PLUGIN_VERSIONYour plugin's version"1.0.0"

Authentication Flow Diagram

Step 4: Connect and Authenticate

Complete connection example with authentication:

// JavaScript/Node.js
const io = require('socket.io-client');

// Read environment variables
const pluginId = process.env.HYPERHQ_PLUGIN_ID;
const socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';
const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;

// Connect to Socket.IO server
const socket = io(`http://localhost:${socketPort}/plugin`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});

let sessionToken = null;

socket.on('connect', () => {
console.log('Connected to HyperHQ Socket.IO server');

// Authenticate with challenge
socket.emit('authenticate', {
pluginId: pluginId,
challenge: authChallenge
});
});

socket.on('authenticated', (response) => {
if (response.success) {
console.log('Authentication successful');
sessionToken = response.sessionToken;

// Now you can register your plugin
socket.emit('plugin:register', {
id: pluginId,
version: '1.0.0',
capabilities: ['socketio', 'gameScanning']
});
} else {
console.error('Authentication failed:', response.error);
}
});

// Include session token in all requests
socket.emit('requestData', {
method: 'getSystems',
params: {},
sessionToken: sessionToken // ✅ Required for security
});

Complete Connection Flow

Overview

The complete plugin connection lifecycle:

Authentication Details

Challenge Expiration

  • Challenges are valid for 30 seconds after plugin launch
  • Each challenge can only be used once
  • If authentication fails, HyperHQ will reject all requests

Session Token

  • Session tokens are generated after successful authentication
  • Tokens are unique per plugin instance (new token on each restart)
  • Tokens expire when the plugin disconnects
  • Must be included in all data requests for security

Security Notes

  • Never hardcode authentication tokens in your plugin
  • Always read the challenge from HYPERHQ_AUTH_CHALLENGE environment variable
  • Store the session token securely in memory (not in files)
  • If authentication fails, log the error and exit gracefully

Event Communication

Standard Events

Incoming Events (HyperHQ → Plugin)

EventDescriptionPayload
requestMethod execution request{id, method, data}
config:updateSettings changed{settings}
system:eventSystem event occurred{type, data}
game:dataGame data update{games[]}
game:launchedGame starting{gameId, systemId}
game:exitedGame ended{gameId, duration}
pingConnection check{timestamp}

Outgoing Events (Plugin → HyperHQ)

EventDescriptionPayload
plugin:registerRegister plugin{id, version, capabilities}
plugin:responseMethod response{id, type, data}
plugin:progressProgress update{task, progress, message}
plugin:notifyUser notification{type, message}
plugin:errorError occurred{error, stack}
plugin:statusStatus change{status, details}
pongConnection reply{timestamp}

Custom Events

You can also define custom events for your plugin:

// Emit custom event
socket.emit('my-plugin:custom:event', {
action: 'custom:action',
data: { /* your data */ }
});

// Listen for custom events
socket.on('my-plugin:response', (data) => {
// Handle response
});

Data Requests

Requesting Data from HyperHQ

Plugins can request various data from HyperHQ:

Get Systems

socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: 'get-systems-001',
sessionToken: sessionToken
});

socket.on('dataResponse', (response) => {
if (response.requestId === 'get-systems-001') {
const systems = response.data; // Array of system objects
console.log('Systems:', systems);
}
});

Get Media Folders

socket.emit('requestData', {
method: 'getMediaFolders',
params: {
systemReferenceId: 'steam',
mediaTypes: ['boxart', 'background', 'screenshot', 'logo']
},
requestId: 'get-folders-001',
sessionToken: sessionToken
});

socket.on('dataResponse', (response) => {
if (response.requestId === 'get-folders-001' && response.success) {
const folders = response.data.folders;
console.log('Media folders:', folders);
}
});

Create System

socket.emit('requestData', {
method: 'createSystem',
params: {
name: 'Steam',
description: 'Steam games library',
platform: 'PC',
referenceId: 'steam',
allowedExtensions: '.exe',
enabled: true
},
requestId: 'create-system-001',
sessionToken: sessionToken
});

socket.on('dataResponse', (response) => {
if (response.requestId === 'create-system-001') {
if (response.success) {
console.log('System created:', response.data.id);
} else {
console.error('Failed:', response.error);
}
}
});

Add Games

socket.emit('requestData', {
method: 'addGames',
params: {
systemId: 'steam',
games: [
{
name: 'Half-Life 2',
fileName: 'hl2.exe',
romPath: 'C:\\Steam\\HL2',
referenceId: 'steam_220',
developer: 'Valve',
enabled: true
}
]
},
requestId: 'add-games-001',
sessionToken: sessionToken
});

socket.on('dataResponse', (response) => {
if (response.requestId === 'add-games-001' && response.success) {
console.log('Games added successfully');
}
});

Progress Reporting

For long-running operations, send progress updates:

async function longOperation() {
const totalSteps = 100;

for (let i = 0; i <= totalSteps; i++) {
// Do work...

// Report progress
socket.emit('plugin:progress', {
task: 'scanning',
taskId: 'scan-123',
progress: i,
total: totalSteps,
message: `Processing item ${i} of ${totalSteps}`,
eta: calculateETA(i, totalSteps)
});

await sleep(100);
}

// Send completion
socket.emit('plugin:progress', {
task: 'scanning',
taskId: 'scan-123',
progress: 100,
total: 100,
message: 'Complete',
completed: true
});
}

Error Handling

Connection Errors

socket.on('connect_error', (error) => {
console.error('Connection failed:', error.message);
// Implement retry logic or fallback
});

socket.on('disconnect', (reason) => {
console.warn('Disconnected:', reason);
if (reason === 'io server disconnect') {
// Server disconnected, try reconnecting
socket.connect();
}
});

socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
// Re-register plugin
registerPlugin();
});

socket.on('reconnect_error', (error) => {
console.error('Reconnection failed:', error);
});

socket.on('reconnect_failed', () => {
console.error('Failed to reconnect after maximum attempts');
// Handle permanent disconnection or show error
});

Request Timeouts

Implement timeouts for requests:

function requestWithTimeout(event, data, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Request timeout: ${event}`));
}, timeoutMs);

socket.emit(event, data);

socket.once(`response:${event}`, (response) => {
clearTimeout(timer);
resolve(response);
});
});
}

// Usage
try {
const games = await requestWithTimeout('request:games', { system: 'all' });
console.log('Received games:', games);
} catch (error) {
console.error('Request failed:', error);
}

Language Examples

JavaScript/Node.js Complete Example

const io = require('socket.io-client');

class HyperHQPlugin {
constructor() {
this.socket = null;
this.connected = false;
this.authenticated = false;
this.sessionToken = null;
this.settings = {};

// Read environment variables
this.pluginId = process.env.HYPERHQ_PLUGIN_ID;
this.authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
this.socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';

if (!this.pluginId || !this.authChallenge) {
console.error('Missing environment variables - plugin must be launched by HyperHQ');
process.exit(1);
}
}

async initialize(settings) {
this.settings = settings;
await this.connect();
return 'initialized';
}

async connect() {
const serverUrl = `http://localhost:${this.socketPort}`;

this.socket = io(`${serverUrl}/plugin`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});

this.socket.on('connect', () => {
this.connected = true;
console.log('Connected to HyperHQ Socket.IO server');

// Authenticate with challenge
this.socket.emit('authenticate', {
pluginId: this.pluginId,
challenge: this.authChallenge
});
});

this.socket.on('authenticated', (response) => {
if (response.success) {
this.authenticated = true;
this.sessionToken = response.sessionToken;
console.log('Authentication successful');
this.register();
} else {
console.error('Authentication failed:', response.error);
process.exit(1);
}
});

this.socket.on('disconnect', () => {
this.connected = false;
this.authenticated = false;
console.log('Disconnected from HyperHQ');
});

this.socket.on('error', (error) => {
console.error('Socket.IO error:', error);
});

// Handle incoming requests from HyperHQ
this.socket.on('request', async (msg) => {
const response = await this.handleRequest(msg);
this.socket.emit('plugin:response', {
id: msg.id,
type: 'response',
data: response,
sessionToken: this.sessionToken
});
});

// Handle data responses
this.socket.on('dataResponse', (response) => {
this.handleDataResponse(response);
});

// Wait for authentication
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Authentication timeout'));
}, 10000);

this.socket.once('authenticated', (response) => {
clearTimeout(timeout);
if (response.success) resolve();
else reject(new Error(response.error));
});
});
}

register() {
this.socket.emit('plugin:register', {
id: this.pluginId,
type: 'executable',
capabilities: ['socketio', 'gameScanning', 'dataSync'],
version: '1.0.0'
});
console.log('Plugin registered with HyperHQ');
}

// Helper to make authenticated data requests
async requestData(method, params) {
if (!this.authenticated) {
throw new Error('Not authenticated');
}

return new Promise((resolve, reject) => {
const requestId = `${method}-${Date.now()}`;
const timeout = setTimeout(() => {
reject(new Error(`Request timeout: ${method}`));
}, 30000);

// Store the resolver for this request
const handler = (response) => {
if (response.requestId === requestId) {
clearTimeout(timeout);
this.socket.off('dataResponse', handler);

if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Request failed'));
}
}
};

this.socket.on('dataResponse', handler);

// Send the request
this.socket.emit('requestData', {
method,
params,
requestId,
sessionToken: this.sessionToken
});

console.log(`Data request sent: ${method} (ID: ${requestId})`);
});
}

handleDataResponse(response) {
console.log('Received data response:', response.requestId);
// Responses are handled by the promise in requestData()
}

async handleRequest(message) {
switch (message.method) {
case 'initialize':
return this.initialize(message.data);
case 'execute':
return await this.execute(message.data);
case 'test':
return this.test();
case 'shutdown':
return this.shutdown();
default:
return { error: `Unknown method: ${message.method}` };
}
}

async execute(data) {
const action = data.action || 'default';

switch (action) {
case 'sync':
return await this.syncGames();
case 'get_status':
return this.getStatus();
default:
return { error: `Unknown action: ${action}` };
}
}

async syncGames() {
try {
// Request systems from HyperHQ
const systems = await this.requestData('getSystems', {});
console.log(`Found ${systems.length} systems`);

// Get media folders for a specific system
if (systems.length > 0) {
const folders = await this.requestData('getMediaFolders', {
systemReferenceId: systems[0].referenceId,
mediaTypes: ['boxart', 'background']
});
console.log('Media folders:', folders);
}

return { success: true, systemCount: systems.length };
} catch (error) {
console.error('Sync failed:', error);
return { error: error.message };
}
}

getStatus() {
return {
connected: this.connected,
authenticated: this.authenticated,
pluginId: this.pluginId
};
}

test() {
return this.connected && this.authenticated;
}

shutdown() {
if (this.socket) {
this.socket.close();
}
return 'ok';
}
}

// Usage
const plugin = new HyperHQPlugin();
plugin.initialize({}).catch(error => {
console.error('Failed to initialize plugin:', error);
process.exit(1);
});

Python Complete Example

import socketio
import asyncio
import json
import os
import sys

class HyperHQPlugin:
def __init__(self):
self.sio = socketio.AsyncClient()
self.connected = False
self.authenticated = False
self.session_token = None
self.settings = {}

# Read environment variables
self.plugin_id = os.environ.get('HYPERHQ_PLUGIN_ID')
self.auth_challenge = os.environ.get('HYPERHQ_AUTH_CHALLENGE')
self.socket_port = os.environ.get('HYPERHQ_SOCKET_PORT', '52789')

# Setup event handlers
self.setup_handlers()

def setup_handlers(self):
@self.sio.on('connect', namespace='/plugin')
async def on_connect():
self.connected = True
print('Connected to HyperHQ Socket.IO server')

# Authenticate with challenge
await self.sio.emit('authenticate', {
'pluginId': self.plugin_id,
'challenge': self.auth_challenge
}, namespace='/plugin')

@self.sio.on('authenticated', namespace='/plugin')
async def on_authenticated(response):
if response.get('success'):
self.authenticated = True
self.session_token = response.get('sessionToken')
print('Authentication successful')
await self.register()
else:
print(f"Authentication failed: {response.get('error')}")
sys.exit(1)

@self.sio.on('disconnect', namespace='/plugin')
def on_disconnect():
self.connected = False
self.authenticated = False
print('Disconnected from HyperHQ')

@self.sio.on('request', namespace='/plugin')
async def on_request(data):
response = await self.handle_request(data)
await self.sio.emit('plugin:response', {
'id': data['id'],
'type': 'response',
'data': response,
'sessionToken': self.session_token
}, namespace='/plugin')

@self.sio.on('config:update', namespace='/plugin')
async def on_config_update(data):
self.handle_config_update(data)

@self.sio.on('game:launched', namespace='/plugin')
async def on_game_launched(data):
self.handle_game_launched(data)

async def initialize(self, settings):
self.settings = settings
await self.connect()
return 'initialized'

async def connect(self):
server_url = f"http://localhost:{self.socket_port}"

await self.sio.connect(
server_url,
namespaces=['/plugin']
)

# Wait for authentication to complete
timeout = 10 # 10 second timeout
start_time = asyncio.get_event_loop().time()
while not self.authenticated:
if asyncio.get_event_loop().time() - start_time > timeout:
raise TimeoutError('Authentication timeout')
await asyncio.sleep(0.1)

async def register(self):
await self.sio.emit('plugin:register', {
'id': self.plugin_id,
'version': '1.0.0',
'capabilities': ['socketio', 'gameScanning']
}, namespace='/plugin')

async def request_data(self, method, params):
"""Helper to make authenticated data requests"""
if not self.authenticated:
raise Exception('Not authenticated')

request_id = f"req_{asyncio.get_event_loop().time()}"

await self.sio.emit('requestData', {
'method': method,
'params': params,
'requestId': request_id,
'sessionToken': self.session_token
}, namespace='/plugin')

# Wait for response (simplified - in production use proper event handling)
# This is just an example pattern
return await self._wait_for_response(request_id)

async def handle_request(self, message):
method = message.get('method')
data = message.get('data', {})

if method == 'execute':
return await self.execute(data)
elif method == 'test':
return self.test()
else:
return {'error': f'Unknown method: {method}'}

async def execute(self, data):
action = data.get('action', 'default')

# Send progress update
await self.sio.emit('plugin:progress', {
'task': action,
'progress': 50,
'message': 'Processing...'
}, namespace='/plugin')

# Do work...
await asyncio.sleep(1)

return {'success': True, 'result': 'completed'}

def test(self):
return self.connected

Troubleshooting

Connection Issues

Plugin cannot connect to Socket.IO server

  1. Check HyperHQ is running

    • Verify HyperHQ is started and the Socket.IO server is active
    • Check HyperHQ logs for Socket.IO server status
  2. Verify server URL

    • Default: http://localhost:52789
    • Check if custom port is configured in HyperHQ
  3. Firewall/Antivirus

    • Add exception for localhost:52789
    • Temporarily disable to test
  4. Check namespace

    • Must use /plugin namespace
    • Example: io('http://localhost:52789/plugin')

Authentication Issues

  1. Authentication Failed - Invalid Challenge

    • Cause: Challenge not found or already used
    • Solution: Ensure you're reading HYPERHQ_AUTH_CHALLENGE from environment variables
    • Solution: Don't cache the challenge - read it fresh on each plugin start
  2. Authentication Timeout

    • Cause: Plugin took too long to authenticate (>30 seconds)
    • Solution: Connect and authenticate immediately after plugin starts
    • Solution: Don't perform long operations before authentication
  3. Session Token Expired

    • Cause: Plugin disconnected and session token is no longer valid
    • Solution: On reconnection, authenticate again with the original challenge
    • Note: Each plugin launch gets a new challenge/session
  4. Missing Environment Variables

    • Check: Verify HyperHQ is passing environment variables
    • Debug: Print process.env.HYPERHQ_AUTH_CHALLENGE at startup
    • Common Issue: Running plugin manually without HyperHQ launching it

Connection drops frequently

  1. Increase reconnection attempts

    "socketio": {
    "reconnectAttempts": 20,
    "reconnectDelay": 3000
    }
  2. Implement heartbeat

    setInterval(() => {
    if (socket.connected) {
    socket.emit('heartbeat', { timestamp: Date.now() });
    }
    }, 30000);
  3. Check for memory leaks

    • Remove event listeners when not needed
    • Clear large data structures

Event Issues

Events not received

  1. Verify event names match exactly

    • Event names are case-sensitive
    • Check for typos
  2. Ensure proper namespace

    // Correct
    socket.on('request', handler);

    // Wrong - no namespace needed in event name
    socket.on('/plugin/request', handler);
  3. Check registration

    • Plugin must register before receiving events
    • Verify registration response

Response timeout

  1. Implement proper async handling

    socket.on('request', async (msg) => {
    try {
    const result = await processRequest(msg);
    socket.emit('plugin:response', { id: msg.id, data: result });
    } catch (error) {
    socket.emit('plugin:error', { id: msg.id, error: error.message });
    }
    });
  2. Send progress for long operations

    // Prevent timeout by sending progress
    const interval = setInterval(() => {
    socket.emit('plugin:progress', { task: 'processing', progress: count });
    }, 1000);

Performance Issues

High CPU usage

  1. Throttle event emissions

    const throttle = (func, delay) => {
    let timeout;
    return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), delay);
    };
    };

    const throttledProgress = throttle((progress) => {
    socket.emit('plugin:progress', progress);
    }, 100);
  2. Use binary for large data

    // Send large data as binary
    const buffer = Buffer.from(largeData);
    socket.binary(true).emit('plugin:binary-data', buffer);

Best Practices

  1. Always implement reconnection logic
  2. Send regular progress updates for long operations
  3. Handle all error cases gracefully
  4. Clean up resources on disconnect
  5. Use structured logging for debugging
  6. Implement request timeouts
  7. Validate all incoming data
  8. Use TypeScript/type hints for better IDE support

Security Best Practices

Authentication Security

  1. Never Hardcode Credentials

    // ❌ WRONG - Don't hardcode tokens
    const authChallenge = "abc123hardcoded";

    // ✅ CORRECT - Always read from environment
    const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
  2. Validate Environment Variables

    // Check that required variables are present
    if (!process.env.HYPERHQ_AUTH_CHALLENGE) {
    console.error('Missing HYPERHQ_AUTH_CHALLENGE - plugin must be launched by HyperHQ');
    process.exit(1);
    }
  3. Store Session Token Securely

    • Keep session token in memory only (never write to disk)
    • Don't log the session token in plain text
    • Clear token on disconnect
  4. Handle Authentication Failures

    socket.on('authenticated', (response) => {
    if (!response.success) {
    console.error('Authentication failed:', response.error);
    // Exit gracefully - don't continue without authentication
    process.exit(1);
    }
    });
  5. Include Session Token in All Requests

    // ✅ Always include session token for security
    socket.emit('requestData', {
    method: 'getSystems',
    params: {},
    sessionToken: this.sessionToken // Required!
    });

    // ❌ WRONG - Requests without token will be rejected
    socket.emit('requestData', {
    method: 'getSystems',
    params: {}
    });

Data Security

  1. Validate All Incoming Data

    socket.on('request', (message) => {
    // Validate message structure
    if (!message.id || !message.method) {
    console.error('Invalid message format');
    return;
    }

    // Validate method is expected
    const allowedMethods = ['initialize', 'execute', 'test', 'shutdown'];
    if (!allowedMethods.includes(message.method)) {
    console.error('Unknown method:', message.method);
    return;
    }
    });
  2. Sanitize User Input

    • Never directly execute user-provided code
    • Validate file paths before accessing files
    • Use parameterized queries for databases
  3. Respect Permissions

    • Only access resources specified in your plugin manifest
    • Don't attempt to bypass permission checks
    • Request minimal permissions needed

Network Security

  1. Use Localhost Only

    • Socket.IO server runs on localhost - never connect to external hosts
    • HyperHQ provides the port via HYPERHQ_SOCKET_PORT
  2. Timeout All Requests

    const timeout = setTimeout(() => {
    reject(new Error('Request timeout'));
    }, 30000);
  3. Handle Connection Errors

    • Implement proper error handling for network failures
    • Don't expose sensitive information in error messages

Next Steps

  • Review the Plugin Developer Guide for complete plugin development information
  • Check the API Reference for all available methods
  • See template examples in templates/ for working implementations
  • Join the HyperHQ Discord for community support

With Socket.IO, your plugin can fully integrate with HyperHQ's real-time ecosystem! 🚀