Skip to main content

Overview

HAPI Hub uses Socket.IO for bidirectional real-time communication between the CLI and hub.

Namespace

CLI connections use the /cli namespace:
ws://127.0.0.1:3006/cli

Connection Process

1. CLI Connects to Hub

import { io } from 'socket.io-client'

const socket = io('http://127.0.0.1:3006/cli', {
  auth: {
    sessionId: 'abc123',  // Optional: join session room
    machineId: 'machine_xyz',  // Optional: join machine room
    namespace: 'default'  // Required: user namespace
  },
  transports: ['websocket', 'polling']
})

2. Authentication

Authentication is performed via the auth object in the handshake:

3. Rooms

When authenticated, the socket automatically joins rooms:
  • session:<sessionId> - Receives updates for this session
  • machine:<machineId> - Receives updates for this machine

Connection Example

import { io, Socket } from 'socket.io-client'
import type { ClientToServerEvents, ServerToClientEvents } from '@hapi/protocol'

type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>

const socket: TypedSocket = io('http://127.0.0.1:3006/cli', {
  auth: {
    sessionId: 'abc123',
    machineId: 'machine_xyz',
    namespace: 'default'
  }
})

socket.on('connect', () => {
  console.log('Connected to hub:', socket.id)
})

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason)
})

socket.on('error', (data) => {
  console.error('Socket error:', data)
})

Event Flow

CLI to Hub

Hub to CLI

Hub to Web

Web clients receive updates via:
  • SSE (GET /api/events) - One-way events
  • REST API - Request/response

Error Handling

The hub emits error events for:

Access Errors

socket.on('error', (data) => {
  if (data.code === 'access-denied') {
    // Session/machine belongs to different namespace
  } else if (data.code === 'not-found') {
    // Session/machine doesn't exist
  } else if (data.code === 'namespace-missing') {
    // Client didn't provide namespace in auth
  }
})

Error Structure

interface SocketError {
  message: string
  code?: 'namespace-missing' | 'access-denied' | 'not-found'
  scope?: 'session' | 'machine'
  id?: string  // Session or machine ID
}

Reconnection

Socket.IO automatically handles reconnection:
socket.io.on('reconnect', (attempt) => {
  console.log('Reconnected after', attempt, 'attempts')
})

socket.io.on('reconnect_attempt', (attempt) => {
  console.log('Reconnection attempt', attempt)
})

socket.io.on('reconnect_failed', () => {
  console.error('Reconnection failed')
})

Custom Reconnection Logic

const socket = io('http://127.0.0.1:3006/cli', {
  auth: { sessionId: 'abc123', namespace: 'default' },
  reconnection: true,
  reconnectionAttempts: 10,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000
})

Heartbeat

The CLI should send periodic heartbeats:

Session Heartbeat

setInterval(() => {
  socket.emit('session-alive', {
    sid: 'abc123',
    time: Date.now(),
    thinking: false,
    mode: 'remote',
    permissionMode: 'default'
  })
}, 5000)  // Every 5 seconds

Machine Heartbeat

setInterval(() => {
  socket.emit('machine-alive', {
    machineId: 'machine_xyz',
    time: Date.now()
  })
}, 10000)  // Every 10 seconds

Ping/Pong

Test connection latency:
socket.emit('ping', () => {
  console.log('Pong received')
})

Transport Options

Socket.IO supports multiple transports:
const socket = io('http://127.0.0.1:3006/cli', {
  transports: ['websocket']
})

WebSocket with Polling Fallback

const socket = io('http://127.0.0.1:3006/cli', {
  transports: ['websocket', 'polling']
})

CORS Configuration

Configure CORS for Socket.IO connections:
CORS_ORIGINS=https://app.example.com,https://other.example.com
# or
CORS_ORIGINS=*
The hub automatically allows:
  • Same-origin requests
  • Origins derived from HAPI_PUBLIC_URL
  • Origins in CORS_ORIGINS

Security

Namespace Isolation

All operations are scoped to the authenticated namespace:
  • Sessions and machines are filtered by namespace
  • Cross-namespace access returns access-denied error
  • Each namespace is isolated (multi-tenant)

No Token Auth

Socket.IO connections don’t use JWT tokens. Instead:
  1. CLI connects with namespace in auth
  2. Hub validates namespace exists
  3. CLI can only access resources in its namespace

Session/Machine Ownership

The hub verifies:
  • Session belongs to the namespace
  • Machine belongs to the namespace
  • No cross-namespace data leakage

Next Steps

Socket Events

Detailed event documentation

RPC

Remote procedure calls

Build docs developers (and LLMs) love