WebSocket APIs
AIQLick uses WebSocket connections for real-time communication across several distinct protocols. This page documents all WebSocket endpoints, their authentication mechanisms, and connection patterns.
GraphQL WebSocket (Backend)
Endpoint: wss://api.aiqlick.com/graphql (production) | wss://api-dev.aiqlick.com/graphql (development)
The Backend GraphQL WebSocket uses the graphql-ws protocol for subscription operations. It powers real-time notifications and administrative log streaming.
Connection
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://api.aiqlick.com/graphql',
connectionParams: {
Authorization: `Bearer ${token}`,
},
lazy: true,
keepAlive: 10_000,
retryAttempts: 5,
shouldRetry: () => true,
});
The backend graphql-ws handler expects connectionParams.Authorization with a Bearer prefix (e.g., Bearer <jwt>). The legacy connectionParams.token format is also accepted for backwards compatibility, but Authorization is the canonical format.
Server-Side Auth Handler (onConnect)
The NestJS GraphQL module processes WebSocket authentication in the graphql-ws.onConnect handler:
- Extracts
Authorization(orauthorization/token) fromconnectionParams - Strips the
Bearerprefix and verifies the JWT usingJwtService - Attaches the token to
extra.request.headers.authorizationso thatJwtAuthGuardcan authenticate the connection - The
contextfunction mapsextra.requesttoreq, making guards work identically for HTTP and WebSocket
// app.module.ts — GraphQL subscription config
subscriptions: {
'graphql-ws': {
onConnect: (ctx) => {
const { connectionParams, extra } = ctx;
const authHeader = connectionParams?.Authorization
|| connectionParams?.authorization
|| connectionParams?.token;
if (!authHeader) return true;
const token = authHeader.replace(/^Bearer\s+/i, '');
if (token && extra?.request) {
const payload = jwtService.verify(token);
extra.request.headers.authorization = `Bearer ${token}`;
}
return true;
},
},
},
context: ({ req, extra }) => {
// graphql-ws passes connection data via extra.request
if (extra?.request) return { req: extra.request };
return { req };
},
Available Subscriptions
| Subscription | Filter | Description |
|---|---|---|
notificationCreated | By authenticated userId | Real-time notification delivery |
unreadCountChanged | By userId, optional companyId | Notification badge count updates |
messageReceived | By authenticated userId | New/edited/deleted messages (includes sender echo) |
unreadMessageCountChanged | By userId, optional companyId | Message unread count updates |
readReceiptChanged | By authenticated userId | Read receipt updates from other participants |
typingIndicatorChanged | By authenticated userId | Typing start/stop events per conversation |
presenceChanged | By authenticated userId | Online/offline status changes |
logStream | Super Admin only | Live system log streaming |
Notification Subscriptions
notificationCreated
Delivers real-time notifications to the authenticated user. Filtered server-side by userId so each user only receives their own notifications.
client.subscribe(
{
query: `subscription {
notificationCreated {
id
type
title
message
isRead
createdAt
}
}`,
},
{
next: (data) => console.log('Notification:', data),
error: (err) => console.error('Subscription error:', err),
complete: () => console.log('Subscription complete'),
}
);
unreadCountChanged
Pushes updated unread notification count whenever notifications are created or marked as read.
client.subscribe(
{
query: `subscription($companyId: ID) {
unreadCountChanged(companyId: $companyId) {
count
companyId
}
}`,
variables: { companyId: 'optional-company-id' },
},
{ next: (data) => updateBadge(data) }
);
Messaging Subscriptions
messageReceived
Delivers new, edited, and deleted messages to all conversation participants, including the sender. The sender's copy includes isSelf: true for the frontend to distinguish own messages.
client.subscribe(
{
query: `subscription {
messageReceived {
id
conversationId
content
status
isEdited
isSelf
sender {
id
firstName
lastName
}
createdAt
}
}`,
},
{
next: ({ data }) => {
const msg = data.messageReceived;
// Append to conversation, deduplicate with existing messages
},
}
);
Redis PubSub channels: messages:user:{userId} for messages, messages:unread:{userId} for counts.
unreadMessageCountChanged
Pushes updated unread message count when new messages arrive or conversations are marked as read.
client.subscribe(
{
query: `subscription {
unreadMessageCountChanged {
count
companyId
}
}`,
},
{
next: ({ data }) => {
updateMessageBadge(data.unreadMessageCountChanged.count);
},
}
);
The Backend uses Redis pub/sub as the subscription transport layer, ensuring that notifications and messages are delivered regardless of which server instance the user is connected to.
GraphQL WebSocket (AI Service)
Endpoint: wss://ai.aiqlick.com/graphql (production) | wss://ai-dev.aiqlick.com/graphql (development)
The AI Service GraphQL WebSocket also uses the graphql-ws protocol. It provides 12 subscriptions for streaming real-time AI processing progress.
Connection
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://ai.aiqlick.com/graphql',
connectionParams: {
token: '<jwt_token>',
},
});
Available Subscriptions
| Subscription | Progress Type | Description |
|---|---|---|
extractCv | Percentage | CV parsing with structured data extraction |
parseJob | Percentage | Job description parsing |
faceExtraction | Percentage | Face detection from CV photos |
addCandidate | Percentage + stage | Full candidate onboarding pipeline |
userJobMatching | Percentage | User-job match scoring |
candidateJobMatching | Percentage | Candidate-job matching |
basicMatching | Percentage | Simple skill matching |
textRewrite | Percentage | AI text rewriting |
generateMeetingInsight | Percentage | Meeting analysis generation |
agentChat | Chunked events | Streaming AI agent chat |
bulkCvImport | Percentage + per-file | Bulk CV import from ZIP file with per-file progress |
taskSubscription | Event-based | System task monitoring |
metricsSubscription | Periodic | System metrics |
See the AI Service API page for detailed subscription schemas and event types.
Transcription WebSocket
Endpoint: wss://ai.aiqlick.com/transcription/ws/{meeting_id}
The Transcription WebSocket receives real-time audio streams from Jigasi (the Jitsi transcription gateway) and processes them through speech-to-text services.
Protocol
This is a custom binary WebSocket protocol, not GraphQL. It carries two types of messages:
- Binary frames: Raw PCM audio data
- Text frames: JSON control messages
Audio Format
| Parameter | Value |
|---|---|
| Encoding | PCM (raw, uncompressed) |
| Sample rate | 16,000 Hz |
| Bit depth | 16-bit |
| Channels | Mono (1 channel) |
| Byte order | Little-endian |
Control Messages
Session start (sent by Jigasi when a participant joins):
{
"type": "session_start",
"meetingId": "room-name",
"participantId": "participant-jid",
"language": "en-US"
}
Session end (sent when a participant leaves):
{
"type": "session_end",
"meetingId": "room-name",
"participantId": "participant-jid"
}
Data Flow
Jitsi Meet (browser) -> JVB (media server) -> Jigasi (transcriber)
-> WebSocket binary PCM -> AI Service -> AWS Transcribe
-> Transcription records saved to PostgreSQL
Transcription results are written to the Transcription table and correlated with Meeting records via the meetingId field (which matches the Jitsi room name).
Voice WebSocket
Endpoint: wss://ai.aiqlick.com/voice/ws/{conversation_id}
The Voice WebSocket enables real-time voice conversations with AI using Amazon Nova Sonic 2.0. It supports bidirectional audio streaming for interactive voice experiences.
Connection
Connect with a conversation ID in the URL path. Authentication is via query parameter:
wss://ai.aiqlick.com/voice/ws/conv-123?token=<jwt_token>
Audio Format
| Parameter | Value |
|---|---|
| Encoding | PCM (raw, uncompressed) |
| Sample rate | 16,000 Hz |
| Bit depth | 16-bit |
| Channels | Mono (1 channel) |
Message Types
Binary frames (bidirectional): Raw PCM audio data. The client sends user speech; the server sends AI-generated speech.
Text frames (JSON control messages):
Client to server:
{
"type": "config",
"language": "en-US",
"voice": "default"
}
Server to client:
{
"type": "transcript",
"text": "How can I help you today?",
"role": "assistant",
"final": true
}
Supported Languages
The Voice WebSocket supports 10 languages through Amazon Nova Sonic:
| Language | Code |
|---|---|
| English (US) | en-US |
| English (UK) | en-GB |
| English (India) | en-IN |
| English (Australia) | en-AU |
| Spanish | es-ES |
| French | fr-FR |
| German | de-DE |
| Italian | it-IT |
| Portuguese (Brazil) | pt-BR |
| Hindi | hi-IN |
Model
The voice service uses Amazon Nova Sonic 2.0 (amazon.nova-2-sonic-v1:0) via AWS Bedrock for real-time speech-to-speech processing.
Authentication Summary
| Endpoint | Auth Method | Token Location |
|---|---|---|
| Backend GraphQL WS | JWT | connectionParams.Authorization (Bearer <token>) |
| AI Service GraphQL WS | JWT | connectionParams.Authorization (Bearer <token>) |
| Transcription WS | Internal | Jigasi service credentials |
| Voice WS | JWT | Query parameter ?token= |
Frontend Apollo Client Integration
The frontend uses Apollo Client's split link to route operations to the correct transport:
import { split } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // WebSocket for subscriptions
httpLink, // HTTP for queries and mutations
);
The frontend maintains separate WebSocket connections:
- Backend subscriptions (notifications, unread counts) connect to
wss://api.aiqlick.com/graphql. - AI subscriptions (CV extraction, matching, agent chat) connect to
wss://ai.aiqlick.com/graphql.
Both connections share the same JWT token and reconnect automatically on disconnection.
Connection Lifecycle
Reconnection
All GraphQL WebSocket connections use the graphql-ws client library, which handles reconnection automatically with exponential backoff. The default retry strategy:
- Immediate first retry
- Exponential backoff with jitter (1s, 2s, 4s, 8s, up to 30s max)
- Unlimited retry attempts
Cleanup
When a WebSocket connection is closed:
- Active subscriptions are terminated server-side.
- Resources allocated for the connection (Redis pub/sub listeners, processing contexts) are cleaned up.
- The client library automatically resubscribes upon reconnection.