Skip to main content

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,
});
Authentication Format

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:

  1. Extracts Authorization (or authorization / token) from connectionParams
  2. Strips the Bearer prefix and verifies the JWT using JwtService
  3. Attaches the token to extra.request.headers.authorization so that JwtAuthGuard can authenticate the connection
  4. The context function maps extra.request to req, 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

SubscriptionFilterDescription
notificationCreatedBy authenticated userIdReal-time notification delivery
unreadCountChangedBy userId, optional companyIdNotification badge count updates
messageReceivedBy authenticated userIdNew/edited/deleted messages (includes sender echo)
unreadMessageCountChangedBy userId, optional companyIdMessage unread count updates
readReceiptChangedBy authenticated userIdRead receipt updates from other participants
typingIndicatorChangedBy authenticated userIdTyping start/stop events per conversation
presenceChangedBy authenticated userIdOnline/offline status changes
logStreamSuper Admin onlyLive 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

SubscriptionProgress TypeDescription
extractCvPercentageCV parsing with structured data extraction
parseJobPercentageJob description parsing
faceExtractionPercentageFace detection from CV photos
addCandidatePercentage + stageFull candidate onboarding pipeline
userJobMatchingPercentageUser-job match scoring
candidateJobMatchingPercentageCandidate-job matching
basicMatchingPercentageSimple skill matching
textRewritePercentageAI text rewriting
generateMeetingInsightPercentageMeeting analysis generation
agentChatChunked eventsStreaming AI agent chat
bulkCvImportPercentage + per-fileBulk CV import from ZIP file with per-file progress
taskSubscriptionEvent-basedSystem task monitoring
metricsSubscriptionPeriodicSystem 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

ParameterValue
EncodingPCM (raw, uncompressed)
Sample rate16,000 Hz
Bit depth16-bit
ChannelsMono (1 channel)
Byte orderLittle-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

ParameterValue
EncodingPCM (raw, uncompressed)
Sample rate16,000 Hz
Bit depth16-bit
ChannelsMono (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:

LanguageCode
English (US)en-US
English (UK)en-GB
English (India)en-IN
English (Australia)en-AU
Spanishes-ES
Frenchfr-FR
Germande-DE
Italianit-IT
Portuguese (Brazil)pt-BR
Hindihi-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

EndpointAuth MethodToken Location
Backend GraphQL WSJWTconnectionParams.Authorization (Bearer <token>)
AI Service GraphQL WSJWTconnectionParams.Authorization (Bearer <token>)
Transcription WSInternalJigasi service credentials
Voice WSJWTQuery 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:

  1. Immediate first retry
  2. Exponential backoff with jitter (1s, 2s, 4s, 8s, up to 30s max)
  3. 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.