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
communicationinplugin.json. - Connect to the root Socket.IO namespace. Do not use
/plugin. - Read the actual port from
HYPERHQ_SOCKET_PORT; do not hardcode52789. - Authenticate immediately. Initial auth uses a one-time challenge; reconnect uses the session token.
- Core request/response events are
requestandresponse.
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_dataanddataResponse/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:
| Variable | Description |
|---|---|
HYPERHQ_PLUGIN_ID | Plugin ID from plugin.json. |
HYPERHQ_PLUGIN_NAME | Plugin name from plugin.json. |
HYPERHQ_PLUGIN_VERSION | Plugin version. |
HYPERHQ_SOCKET_PORT | Active Socket.IO server port. |
HYPERHQ_AUTH_CHALLENGE | One-time auth challenge for the first connection. |
PLUGIN_SETTINGS | Launcher-provided compatibility value. Do not treat it as authoritative; use initialize(data).settings. |
Authentication and reconnect
Initial authentication
- HyperHQ launches the plugin and creates a challenge.
- The plugin connects to
http://localhost:${HYPERHQ_SOCKET_PORT}. - The plugin emits
authenticatewith{ pluginId, challenge }. - HyperHQ validates the challenge and emits
authenticatedwith asessionToken.
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:
| Method | Behavior | Notes |
|---|---|---|
getSystems | Blocking | Returns systems. |
getMediaFolders | Blocking | Returns media folder data for a requested system/media type set. |
getGamesForSystem | Blocking | Accepts flexible identifiers passed through to the frontend handler (ID, reference ID, or name where supported). |
createSystem | Fire-and-forget | Immediate acknowledgement; actual creation continues asynchronously. |
createEmulator | Fire-and-forget | Immediate acknowledgement. |
addGames | Fire-and-forget | Can pass richer game payloads including filenames/paths, reference IDs, and metadata. |
removeGames | Fire-and-forget | Removal request; include stable game identifiers where possible. |
launchGame | Fire-and-forget | Starts 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:
sessionTokenis mandatory and strictly checked.filePathmust 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
sessionTokenin memory. Do not write it to disk. - Do not log OAuth tokens, API keys, auth challenges, or session tokens.
- Validate every incoming
request.methodbefore 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: truewhen the user can fix the problem in settings. - Stop cleanly on repeated auth failure instead of reconnecting forever.
Troubleshooting
| Problem | Check |
|---|---|
| Cannot connect | Confirm the plugin reads HYPERHQ_SOCKET_PORT and connects to http://localhost:${port} with no namespace. |
| Auth fails | Authenticate within 30 seconds, use the env challenge only once, and use the exact HYPERHQ_PLUGIN_ID. |
| Reconnect fails | Reconnect with { pluginId, sessionToken } within the 30-second grace period. |
| Data request times out | Listen for both dataResponse and data_response; verify requestId matches. |
| Events not received | Emit subscribeEvents after authentication and listen for hyperHqEvent. |
| File request rejected | Include sessionToken, keep the file under the plugin directory, and stay below 10 MB. |
| Response ignored | Echo the incoming request id exactly in the response payload. |
| Plugin starts outside HyperHQ but fails inside HyperHQ | Confirm executable permissions, working directory assumptions, and plugin.json paths. |