Skip to main content

HyperHQ Plugin Socket.IO Guide

This guide covers the current Socket.IO runtime for executable plugins that need real-time communication with HyperHQ.

Key points

  • Configure Socket.IO under communication in plugin.json.
  • Connect to the root Socket.IO namespace. Do not use /plugin.
  • Read the actual port from HYPERHQ_SOCKET_PORT; do not hardcode 52789.
  • Authenticate immediately. Initial auth uses a one-time challenge; reconnect uses the session token.
  • Core request/response events are request and response.

Socket.IO plugins receive current settings in the initialize request payload. Treat environment variables as launch metadata, not the source of truth for settings.

Leaderboard provider plugins can still use Socket.IO; advertise the leaderboard-provider capability and respond to the leaderboard methods documented in the API Reference.

  • Data request aliases are supported for compatibility: requestData/request_data and dataResponse/data_response.

Manifest configuration

{
"id": "my-socket-plugin",
"name": "My Socket Plugin",
"version": "1.0.0",
"description": "Realtime integration example",
"author": "Your Name",
"type": "executable",
"executable": "plugin.exe",
"executableProviders": {
"windows": "plugin.exe",
"linux": "plugin",
"macos": "plugin"
},
"communication": {
"preferred": "socketio",
"fallback": "stdio",
"stdio": { "enabled": true },
"socketio": { "enabled": true }
},
"capabilities": [
{ "name": "realtime", "description": "Uses Socket.IO", "required": true }
],
"permissions": [
{ "type": "network", "scope": "localhost", "description": "Connect to HyperHQ Socket.IO" }
]
}

The launcher chooses Socket.IO when communication.socketio.enabled is true, a Socket.IO server is available, and either preferred or fallback is socketio. Otherwise it falls back to stdio.

Environment variables

Socket.IO plugins launched by HyperHQ receive:

VariableDescription
HYPERHQ_PLUGIN_IDPlugin ID from plugin.json.
HYPERHQ_PLUGIN_NAMEPlugin name from plugin.json.
HYPERHQ_PLUGIN_VERSIONPlugin version.
HYPERHQ_SOCKET_PORTActive Socket.IO server port.
HYPERHQ_AUTH_CHALLENGEOne-time auth challenge for the first connection.
PLUGIN_SETTINGSLauncher-provided compatibility value. Do not treat it as authoritative; use initialize(data).settings.

Authentication and reconnect

Initial authentication

  1. HyperHQ launches the plugin and creates a challenge.
  2. The plugin connects to http://localhost:${HYPERHQ_SOCKET_PORT}.
  3. The plugin emits authenticate with { pluginId, challenge }.
  4. HyperHQ validates the challenge and emits authenticated with a sessionToken.

The challenge expires after 30 seconds and can only be used once.

Reconnect

If the socket disconnects, HyperHQ keeps the session token and subscriptions for a 30-second grace period. Reconnect with:

socket.emit('authenticate', { pluginId, sessionToken });

Do not reuse the original challenge for reconnect. If the grace period expires, allow HyperHQ to restart the plugin so it receives a new challenge.

JavaScript client example

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

const pluginId = process.env.HYPERHQ_PLUGIN_ID;
const pluginName = process.env.HYPERHQ_PLUGIN_NAME;
const pluginVersion = process.env.HYPERHQ_PLUGIN_VERSION;
const port = process.env.HYPERHQ_SOCKET_PORT;
const challenge = process.env.HYPERHQ_AUTH_CHALLENGE;
if (!pluginId || !port || !challenge) {
console.error('This plugin must be launched by HyperHQ.');
process.exit(1);
}

let sessionToken = null;
let authenticated = false;

const socket = io(`http://localhost:${port}`, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});

socket.on('connect', () => {
if (sessionToken) {
socket.emit('authenticate', { pluginId, sessionToken });
} else {
socket.emit('authenticate', { pluginId, challenge });
}
});

socket.on('authenticated', (msg) => {
if (!msg.success) {
console.error('Authentication failed:', msg.error);
process.exit(1);
}

sessionToken = msg.sessionToken;
authenticated = true;
console.log(`${pluginName} ${pluginVersion} authenticated`, msg.reconnected ? '(reconnected)' : '');

socket.emit('subscribeEvents', ['gameLaunched', 'gameClosed', 'systemChanged']);
});

socket.on('request', async (message) => {
try {
const data = await handleRequest(message.method, message.data || {});
socket.emit('response', {
id: message.id,
type: 'response',
data,
timestamp: Date.now()
});
} catch (error) {
socket.emit('response', {
id: message.id,
type: 'error',
error: { message: error.message, recoverable: true },
timestamp: Date.now()
});
}
});

socket.on('hyperHqEvent', (event) => {
console.log('HyperHQ event:', event.type, event.data);
});

async function handleRequest(method, data) {
switch (method) {
case 'initialize': return 'initialized';
case 'execute': return { success: true, action: data.action || 'default' };
case 'test': return true;
case 'shutdown': return 'ok';
default: throw new Error(`Unsupported method: ${method}`);
}
}

Python client outline

import asyncio, json, os, socketio, sys

plugin_id = os.environ.get('HYPERHQ_PLUGIN_ID')
port = os.environ.get('HYPERHQ_SOCKET_PORT')
challenge = os.environ.get('HYPERHQ_AUTH_CHALLENGE')
session_token = None

sio = socketio.AsyncClient(reconnection=True)

@sio.event
async def connect():
global session_token
if session_token:
await sio.emit('authenticate', {'pluginId': plugin_id, 'sessionToken': session_token})
else:
await sio.emit('authenticate', {'pluginId': plugin_id, 'challenge': challenge})

@sio.on('authenticated')
async def authenticated(message):
global session_token
if not message.get('success'):
print(f"Authentication failed: {message.get('error')}", file=sys.stderr)
await sio.disconnect()
return
session_token = message.get('sessionToken')
await sio.emit('subscribeEvents', ['gameLaunched'])

@sio.on('request')
async def request(message):
method = message.get('method')
if method == 'test':
result = True
elif method == 'initialize':
result = 'initialized'
elif method == 'execute':
result = {'success': True}
else:
result = {'error': f'Unsupported method: {method}'}
await sio.emit('response', {'id': message.get('id'), 'type': 'response', 'data': result})

@sio.on('hyperHqEvent')
async def hyper_hq_event(event):
print('event', event)

async def main():
if not plugin_id or not port or not challenge:
raise RuntimeError('This plugin must be launched by HyperHQ')
await sio.connect(f'http://localhost:{port}', transports=['websocket', 'polling'])
await sio.wait()

asyncio.run(main())

Core request/response

HyperHQ sends plugin method calls on request:

{
"id": "msg_123",
"type": "request",
"method": "execute",
"data": { "action": "sync" },
"timestamp": 1710000000000
}

Plugins respond on response with the same id:

{
"id": "msg_123",
"type": "response",
"data": { "success": true },
"timestamp": 1710000001000
}

Data requests

Use data requests when the plugin needs HyperHQ library data or wants HyperHQ to perform library operations.

function requestData(method, params = {}) {
return new Promise((resolve, reject) => {
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;

const handler = (response) => {
if (response.requestId !== requestId) return;
socket.off('dataResponse', handler);
socket.off('data_response', handler);
response.success ? resolve(response.data) : reject(new Error(response.error));
};

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

socket.emit('requestData', { method, params, requestId, sessionToken });
});
}

const systems = await requestData('getSystems');

Supported methods:

MethodBehaviorNotes
getSystemsBlockingReturns systems.
getMediaFoldersBlockingReturns media folder data for a requested system/media type set.
getGamesForSystemBlockingAccepts flexible identifiers passed through to the frontend handler (ID, reference ID, or name where supported).
createSystemFire-and-forgetImmediate acknowledgement; actual creation continues asynchronously.
createEmulatorFire-and-forgetImmediate acknowledgement.
addGamesFire-and-forgetCan pass richer game payloads including filenames/paths, reference IDs, and metadata.
removeGamesFire-and-forgetRemoval request; include stable game identifiers where possible.
launchGameFire-and-forgetStarts game launch and returns immediate acknowledgement.

Blocking methods time out after 30 seconds if the frontend does not answer. Fire-and-forget methods only confirm the request was accepted.

Event subscriptions and broadcasts

Subscribe after authentication:

socket.emit('subscribeEvents', ['gameLaunched', 'gameClosed', 'systemChanged']);

socket.on('eventsSubscribed', ({ events }) => {
console.log('Subscribed:', events);
});

socket.on('hyperHqEvent', ({ type, data, timestamp }) => {
console.log(type, data, timestamp);
});

Common broadcast event types include gameLaunched, gameClosed, romSelected, systemChanged, mediaProgress, mediaDownloadStart, mediaDownloadFinish, serverMessageReceived, and dbConnected.

Requesting packaged files

Plugins can request files from their own plugin installation directory.

socket.emit('requestFile', {
requestId: 'schema-1',
filePath: 'assets/schema.json',
sessionToken
});

socket.on('fileData', (message) => {
if (!message.success) {
console.error(message.error);
return;
}
const content = Buffer.from(message.data, 'base64').toString('utf8');
});

Constraints:

  • sessionToken is mandatory and strictly checked.
  • filePath must resolve inside the plugin directory.
  • Maximum response size is 10 MB.
  • Data is base64 encoded.

Progress/status updates

For long operations, emit statusUpdate periodically. HyperHQ uses these updates for observability and to keep active request timeouts fresh where applicable.

socket.emit('statusUpdate', {
status: 'syncing',
message: 'Imported 120 of 500 games'
});

Security and reliability checklist

  • Keep sessionToken in memory. Do not write it to disk.
  • Do not log OAuth tokens, API keys, auth challenges, or session tokens.
  • Validate every incoming request.method before running code.
  • Validate payload shape and reject unknown fields when the action changes files, launches games, or calls external services.
  • Use the smallest useful plugin permissions in plugin.json.
  • Add timeouts for outbound HTTP calls and long-running plugin work.
  • Return structured errors with recoverable: true when the user can fix the problem in settings.
  • Stop cleanly on repeated auth failure instead of reconnecting forever.

Troubleshooting

ProblemCheck
Cannot connectConfirm the plugin reads HYPERHQ_SOCKET_PORT and connects to http://localhost:${port} with no namespace.
Auth failsAuthenticate within 30 seconds, use the env challenge only once, and use the exact HYPERHQ_PLUGIN_ID.
Reconnect failsReconnect with { pluginId, sessionToken } within the 30-second grace period.
Data request times outListen for both dataResponse and data_response; verify requestId matches.
Events not receivedEmit subscribeEvents after authentication and listen for hyperHqEvent.
File request rejectedInclude sessionToken, keep the file under the plugin directory, and stay below 10 MB.
Response ignoredEcho the incoming request id exactly in the response payload.
Plugin starts outside HyperHQ but fails inside HyperHQConfirm executable permissions, working directory assumptions, and plugin.json paths.