Skip to content

Nativecode-Co/GoRing

 
 

Repository files navigation

Goring - WebSocket Signaling Server for Voice Calling

A production-ready Golang WebSocket signaling server for voice calling with Redis-backed state management and WebRTC support.

Features

  • Single WebSocket per user - Enforced via Redis with atomic SET NX
  • Redis as single source of truth - No in-memory session state
  • JWT authentication - Validated during WebSocket upgrade
  • User profile info - Name, username, and profile image propagated in call events
  • Cross-instance messaging - Redis pub/sub for horizontal scaling
  • Race-condition safe - Lua scripts for atomic state transitions
  • Graceful shutdown - Clean connection handling on SIGINT/SIGTERM
  • Full WebRTC signaling - SDP offer/answer and ICE candidate exchange
  • Push notifications for offline callees - FCM (Android) and APNs VoIP (iOS) when callee is not connected
  • Cancel notifications - Automatic cancel push when caller hangs up before callee answers, plus call.check for session validation
  • Working example client - React + TypeScript web client included

Architecture

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client A  │     │   Client B  │     │   Client C  │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │
       │ WebSocket         │ WebSocket         │ WebSocket
       │                   │                   │
       ▼                   ▼                   ▼
┌──────────────────────────────────────────────────────┐
│                    Server Instance 1                  │
│  ┌─────────┐  ┌────────────┐  ┌──────────────────┐  │
│  │   Hub   │──│CallManager │──│ SessionManager   │  │
│  └────┬────┘  └────────────┘  └────────┬─────────┘  │
│       │                                │             │
│       └────────────┬───────────────────┘             │
│                    │                                 │
└────────────────────┼─────────────────────────────────┘
                     │
                     ▼ Redis
┌──────────────────────────────────────────────────────┐
│  ┌─────────────────┐  ┌──────────────────────────┐  │
│  │  Sessions/Keys   │  │   Pub/Sub Channels       │  │
│  │  ws:user:*       │  │   ws:signal:*            │  │
│  │  call:session:*  │  │                          │  │
│  │  call:user:*     │  │                          │  │
│  └─────────────────┘  └──────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Project Structure

goring/
├── cmd/server/            # Application entry point
├── internal/
│   ├── auth/             # JWT token validation
│   ├── notification/     # Push notification backends (FCM, APNs)
│   ├── protocol/         # WebSocket message definitions
│   ├── redis/            # Redis client and session management
│   ├── signaling/        # Call logic and pub/sub
│   └── ws/               # WebSocket hub and client handlers
├── examples/
│   └── web-client/       # React + TypeScript example client
│       ├── src/
│       │   ├── lib/SignalingClient.ts   # WebRTC/WebSocket client
│       │   ├── components/CallUI.tsx     # Call interface component
│       │   └── App.tsx                   # Main application
│       └── package.json
├── docker-compose.yml    # Docker orchestration
├── Dockerfile           # Multi-stage build
└── .env.example         # Configuration template

Quick Start

Option 1: Try the Example Client (Recommended for Testing)

The fastest way to see Goring in action is to use the included React example client:

# Start server and Redis with Docker
docker compose up -d

# In a separate terminal, run the example web client
cd examples/web-client
npm install
npm run dev

Open the URL shown (typically http://localhost:5173) in two different browser tabs. You can now make test calls between the tabs using the demo JWT tokens provided in the UI.

Option 2: Docker Only

# Start server with Redis
docker compose up -d

# Test horizontal scaling (two server instances)
docker compose --profile scaling up -d

# View logs
docker compose logs -f server

# Stop
docker compose down

The server will be available at ws://localhost:8080/ws?token=<JWT>

Option 3: Local Development

Prerequisites: Go 1.21+, Redis 6+

# Set environment variables
export JWT_SECRET="your-secret-key"
export REDIS_ADDR="localhost:6379"
export PORT="8080"

# Run the server
go run ./cmd/server

Testing with JWT Tokens

For development and testing, you can generate test JWT tokens:

// The auth package includes a test token generator
// In production, use your auth service to generate tokens

// Example JWT payload structure:
{
  "hash": "user-123",              // User ID (required)
  "name": "Alice",                 // Display name (optional)
  "username": "alice",             // Username (optional)
  "image_profile": "https://...",  // Profile image URL (optional)
  "iat": 1768720767,
  "exp": 1768732667                // Token expiration
}

Connect via WebSocket using wscat or any WebSocket client:

wscat -c "ws://localhost:8080/ws?token=<JWT>"

Configuration

Environment Variables

Variable Default Description
PORT 8080 HTTP server port
JWT_SECRET dev-secret-change-in-production Secret key for JWT validation
REDIS_ADDR localhost:6379 Redis server address
REDIS_PASSWORD (empty) Redis password
SERVER_ID (hostname) Unique server instance identifier

Push Notifications (Optional)

Configure FCM and/or APNs to notify offline callees. If neither is set, calling an offline user returns a user_offline error.

Android — Firebase Cloud Messaging (FCM v1 API)
Variable Description
FCM_SERVICE_ACCOUNT_JSON Full contents of the Firebase service account JSON file
FCM_PROJECT_ID Firebase project ID (e.g. my-firebase-project)
iOS — Apple Push Notification service (VoIP, certificate-based)
Variable Description
APNS_CERT_PATH Path to the .p12 VoIP certificate file
APNS_CERT_PASSPHRASE Certificate passphrase
APNS_BUNDLE_ID App bundle ID (e.g. com.example.app); topic sent as <bundle_id>.voip
APNS_PRODUCTION true for production APNs endpoint, false for sandbox

Environment Files

# Copy the example file
cp .env.example .env

# Edit with your values
nano .env

The .env file is gitignored. Docker Compose automatically loads it.

WebSocket Protocol

All messages follow this envelope format:

{
  "type": "event.name",
  "payload": {}
}

Call Flow with WebRTC Signaling

Callee online

Caller                    Server                    Callee
   │                         │                         │
   │──call.start────────────▶│                         │
   │  {callee_id, ...}       │──call.ring─────────────▶│
   │◀───call.ringing─────────│  {session_id, caller_id,│
   │  {session_id, callee_id}│   caller_info}          │
   │                         │                         │
   │                         │◀────────call.accept─────│
   │◀───call.accepted────────│  {session_id}           │
   │  {session_id,           │                         │
   │   callee_info}          │                         │
   │                         │                         │
   │  ─────────── WebRTC Signaling Phase ───────────  │
   │                         │                         │
   │──webrtc.offer──────────▶│──webrtc.offer─────────▶│
   │◀─────────webrtc.answer──│◀─────────webrtc.answer─│
   │──webrtc.ice────────────▶│──webrtc.ice───────────▶│
   │◀─────────────webrtc.ice─│◀────────────webrtc.ice─│
   │                         │                         │
   │  ═══════════ P2P Media Connection ═══════════   │
   │                         │                         │
   │──call.end──────────────▶│──call.ended────────────▶│
   │                         │  {session_id, reason}   │

Callee offline (push notification fallback)

Caller                    Server              FCM/APNs          Callee App
   │                         │                    │                  │
   │──call.start────────────▶│                    │                  │
   │  {callee_id,            │──IsUserOnline?─────▶Redis             │
   │   callee_device_token,  │◀── offline ────────│                  │
   │   callee_os}            │──CreateSession─────▶Redis             │
   │◀───call.ringing─────────│                    │                  │
   │  {session_id, callee_id}│──[goroutine]───────▶                  │
   │                         │  SendCallNotif     │──push───────────▶│
   │                         │                    │  (VoIP/FCM)      │
   │                         │◀─────────WebSocket connect ──────────│
   │                         │◀─────────call.check ─────────────────│
   │                         │──call.check_result──────────────────▶│
   │                         │  {exists: true}    │                  │
   │                         │◀─────────call.accept ────────────────│
   │◀───call.accepted────────│                    │                  │

Caller hangs up before callee wakes (cancel notification)

Caller                    Server              FCM/APNs          Callee App
   │                         │                    │                  │
   │──call.start────────────▶│                    │                  │
   │◀───call.ringing─────────│──push──────────────▶──────────────────│
   │                         │  (incoming_call)   │                  │
   │──call.end──────────────▶│                    │                  │
   │                         │──push──────────────▶──────────────────│
   │                         │  (call_cancelled)  │                  │
   │                         │──DeleteSession─────▶Redis             │
   │                         │                    │                  │
   │                         │          If cancel push arrives:      │
   │                         │          → App dismisses call UI      │
   │                         │                    │                  │
   │                         │          If cancel push missed:       │
   │                         │◀─────────WebSocket connect ──────────│
   │                         │◀─────────call.check ─────────────────│
   │                         │──call.check_result──────────────────▶│
   │                         │  {exists: false}   │   → Stop ringing│

Message Types

Client → Server

call.start - Initiate a call

{
  "type": "call.start",
  "payload": {
    "callee_id": "user-456",
    "callee_device_token": "fcm-or-apns-token",
    "callee_os": "android"
  }
}

callee_device_token and callee_os ("android" or "ios") are optional. When provided and the callee is offline, the server sends a push notification instead of returning user_offline.

call.accept - Accept incoming call

{
  "type": "call.accept",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

call.reject - Reject incoming call

{
  "type": "call.reject",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

call.end - End active call

{
  "type": "call.end",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

call.check - Validate session before ringing (for push-woken clients)

When a callee's app wakes from a push notification, it should send call.check to verify the call is still active before showing the ringing UI. This handles the race condition where the caller hangs up before the callee's app wakes up.

{
  "type": "call.check",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Response (call.check_result):

// Session still active
{
  "type": "call.check_result",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "exists": true,
    "state": "ringing",
    "caller_id": "user-123"
  }
}

// Session no longer exists (caller hung up)
{
  "type": "call.check_result",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "exists": false
  }
}

WebRTC Signaling (Bidirectional)

These messages are forwarded to the peer. Only valid after call is accepted.

webrtc.offer - Send SDP offer (caller → callee)

{
  "type": "webrtc.offer",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "sdp": "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\n..."
  }
}

webrtc.answer - Send SDP answer (callee → caller)

{
  "type": "webrtc.answer",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "sdp": "v=0\r\no=- 789012 2 IN IP4 127.0.0.1\r\n..."
  }
}

webrtc.ice - Send ICE candidate (both directions)

{
  "type": "webrtc.ice",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "candidate": "candidate:1 1 UDP 2122252543 192.168.1.1 12345 typ host"
  }
}

Server → Client

call.ringing - Call is ringing (sent to caller)

{
  "type": "call.ringing",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "callee_id": "user-456"
  }
}

call.ring - Incoming call notification (sent to callee)

{
  "type": "call.ring",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "caller_id": "user-123",
    "caller_info": {
      "user_id": "user-123",
      "name": "Alice",
      "username": "alice",
      "image_profile": "https://example.com/alice.jpg"
    }
  }
}

call.accepted - Call was accepted

{
  "type": "call.accepted",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "callee_info": {
      "user_id": "user-456",
      "name": "Bob",
      "username": "bob",
      "image_profile": "https://example.com/bob.jpg"
    }
  }
}

call.rejected - Call was rejected

{
  "type": "call.rejected",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "callee_info": {
      "user_id": "user-456",
      "name": "Bob",
      "username": "bob",
      "image_profile": "https://example.com/bob.jpg"
    }
  }
}

call.ended - Call has ended

{
  "type": "call.ended",
  "payload": {
    "session_id": "550e8400-e29b-41d4-a716-446655440000",
    "reason": "ended_by_user",
    "peer_info": {
      "user_id": "user-456",
      "name": "Bob",
      "username": "bob",
      "image_profile": "https://example.com/bob.jpg"
    }
  }
}

error - Error occurred

{
  "type": "error",
  "payload": {
    "code": "user_busy",
    "message": "User is busy"
  }
}

Error Codes

Code Description
invalid_message Malformed message or unknown type
unauthorized Not authorized for this operation
user_busy Target user is already in a call
user_offline Target user is not connected and no device token was provided
session_not_found Call session does not exist
invalid_state Invalid state transition (e.g., double accept)
internal_error Server-side error

Redis Data Model

Key Pattern Type TTL Purpose
ws:user:{user_id} STRING 30s User's WebSocket server ID
call:session:{session_id} HASH 120s Call session state
call:user:{user_id} STRING 120s User's active call session

call:session Hash Fields

caller_id    - ID of the user who initiated the call
callee_id    - ID of the user being called
state        - Current state: ringing | accepted | rejected | ended
created_at   - Unix timestamp of session creation
device_token - Callee's push notification token (empty if callee was online)
device_os    - Callee's device OS: android | ios (empty if callee was online)

Horizontal Scaling

The server supports horizontal scaling via Redis pub/sub:

  1. Each instance subscribes to channels for its connected users
  2. When sending a message to a user on another instance, publish to their channel
  3. The instance with the connection receives and forwards the message
┌──────────────┐          ┌──────────────┐
│  Instance 1  │          │  Instance 2  │
│  (User A)    │          │  (User B)    │
└──────┬───────┘          └──────┬───────┘
       │                         │
       │    Redis Pub/Sub        │
       │  ws:signal:userB        │
       └─────────────────────────┘

Health Check

curl http://localhost:8080/health

Returns 200 OK if the server and Redis are healthy.

Example Client

The project includes a fully functional React + TypeScript web client that demonstrates complete WebRTC integration.

Features of Example Client

  • Complete call lifecycle (initiate, ring, accept/reject, end)
  • Real-time WebRTC audio calling
  • User profile display (name, username, avatar) for callers and peers
  • Call duration timer
  • Connection status indicators
  • Error handling with user-friendly messages
  • Microphone permission handling

Running the Example

# Start the server
docker compose up -d

# Install and run the example client
cd examples/web-client
npm install
npm run dev

Open two browser tabs with the provided URL to test calls between different users.

Example Client Architecture

The example client consists of:

  • SignalingClient.ts - Core WebSocket and WebRTC integration

    • Manages WebSocket connection to signaling server
    • Handles WebRTC peer connection setup
    • State machine for call lifecycle
    • SDP offer/answer negotiation
    • ICE candidate exchange
    • UserInfo interface for profile data in events
  • CallUI.tsx - React UI component

    • Dynamic UI based on call state
    • Incoming call notifications with caller profile
    • Active call display with peer avatar and name
    • Active call timer
    • Call controls
  • App.tsx - Main application

    • Connection management
    • JWT token input
    • Server URL configuration

Development

# Build the Go server
go build -o server ./cmd/server

# Run with verbose logging
./server

# Run tests
go test ./...

# Build example client for production
cd examples/web-client
npm run build

Client Integration Guide

JWT Token Format

The server expects JWT tokens with a hash claim as the user ID. Profile fields (name, username, image_profile) are extracted and included in call events:

{
  "hash": "6217138344386500",
  "name": "User Name",
  "username": "username",
  "image_profile": "https://example.com/avatar.jpg",
  "user_type": "16587740479514765",
  "is_admin": "0",
  "iat": 1768720767,
  "exp": 1768732667
}

Note: A complete, working example client is available in examples/web-client. The code snippets below show the core integration pattern.

JavaScript/TypeScript Example

class SignalingClient {
  private ws: WebSocket;
  private sessionId: string | null = null;
  private pc: RTCPeerConnection | null = null;

  constructor(private serverUrl: string, private token: string) {}

  connect() {
    this.ws = new WebSocket(`${this.serverUrl}/ws?token=${this.token}`);
    this.ws.onmessage = (event) => this.handleMessage(JSON.parse(event.data));
  }

  private handleMessage(msg: { type: string; payload: any }) {
    switch (msg.type) {
      case 'call.ringing':
        // Call is ringing - save session_id to allow cancelling
        this.sessionId = msg.payload.session_id;
        this.onCallRinging?.(msg.payload.session_id, msg.payload.callee_id);
        break;

      case 'call.ring':
        // Incoming call - show UI to accept/reject
        // caller_info contains: user_id, name, username, image_profile
        this.sessionId = msg.payload.session_id;
        this.onIncomingCall?.(msg.payload.caller_id, msg.payload.session_id, msg.payload.caller_info);
        break;

      case 'call.accepted':
        // Call accepted - start WebRTC as caller
        // callee_info contains: user_id, name, username, image_profile
        this.sessionId = msg.payload.session_id;
        this.onCallAccepted?.(msg.payload.callee_info);
        this.startWebRTC(true);
        break;

      case 'webrtc.offer':
        // Received offer - create answer
        this.handleOffer(msg.payload.sdp);
        break;

      case 'webrtc.answer':
        // Received answer - set remote description
        this.pc?.setRemoteDescription({ type: 'answer', sdp: msg.payload.sdp });
        break;

      case 'webrtc.ice':
        // Received ICE candidate
        if (msg.payload.candidate) {
          this.pc?.addIceCandidate({ candidate: msg.payload.candidate });
        }
        break;

      case 'call.ended':
        // peer_info contains info about the user who ended the call
        this.cleanup();
        this.onCallEnded?.(msg.payload.reason, msg.payload.peer_info);
        break;

      case 'error':
        this.onError?.(msg.payload.code, msg.payload.message);
        break;
    }
  }

  // Initiate a call
  startCall(calleeId: string) {
    this.send('call.start', { callee_id: calleeId });
  }

  // Accept incoming call
  acceptCall(sessionId: string) {
    this.sessionId = sessionId;
    this.send('call.accept', { session_id: sessionId });
    this.startWebRTC(false); // Start as callee
  }

  // Reject incoming call
  rejectCall(sessionId: string) {
    this.send('call.reject', { session_id: sessionId });
  }

  // End active call
  endCall() {
    if (this.sessionId) {
      this.send('call.end', { session_id: this.sessionId });
      this.cleanup();
    }
  }

  private async startWebRTC(isCaller: boolean) {
    this.pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    // Handle ICE candidates
    this.pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.send('webrtc.ice', {
          session_id: this.sessionId,
          candidate: event.candidate.candidate
        });
      }
    };

    // Handle incoming tracks
    this.pc.ontrack = (event) => {
      this.onRemoteStream?.(event.streams[0]);
    };

    // Add local audio track
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    stream.getTracks().forEach(track => this.pc!.addTrack(track, stream));

    if (isCaller) {
      // Create and send offer
      const offer = await this.pc.createOffer();
      await this.pc.setLocalDescription(offer);
      this.send('webrtc.offer', {
        session_id: this.sessionId,
        sdp: offer.sdp
      });
    }
  }

  private async handleOffer(sdp: string) {
    if (!this.pc) return;
    await this.pc.setRemoteDescription({ type: 'offer', sdp });
    const answer = await this.pc.createAnswer();
    await this.pc.setLocalDescription(answer);
    this.send('webrtc.answer', {
      session_id: this.sessionId,
      sdp: answer.sdp
    });
  }

  private send(type: string, payload: any) {
    this.ws.send(JSON.stringify({ type, payload }));
  }

  private cleanup() {
    this.pc?.close();
    this.pc = null;
    this.sessionId = null;
  }

  // Event callbacks - user info includes: user_id, name, username, image_profile
  onCallRinging?: (sessionId: string, calleeId: string) => void;
  onIncomingCall?: (callerId: string, sessionId: string, callerInfo?: UserInfo) => void;
  onCallAccepted?: (calleeInfo?: UserInfo) => void;
  onCallEnded?: (reason: string, peerInfo?: UserInfo) => void;
  onRemoteStream?: (stream: MediaStream) => void;
  onError?: (code: string, message: string) => void;
}

interface UserInfo {
  user_id: string;
  name?: string;
  username?: string;
  image_profile?: string;
}

// Usage
const client = new SignalingClient('ws://localhost:8080', 'your-jwt-token');
client.onCallRinging = (sessionId, calleeId) => {
  // Call is now ringing - can use sessionId to cancel with endCall()
  console.log(`Calling ${calleeId}, session: ${sessionId}`);
};
client.onIncomingCall = (callerId, sessionId, callerInfo) => {
  // Display caller's name and profile image if available
  const callerName = callerInfo?.name || callerInfo?.username || callerId;
  if (confirm(`Incoming call from ${callerName}`)) {
    client.acceptCall(sessionId);
  } else {
    client.rejectCall(sessionId);
  }
};
client.onCallAccepted = (calleeInfo) => {
  // Display callee's profile info when call is accepted
  console.log(`Call accepted by ${calleeInfo?.name || calleeInfo?.username}`);
};
client.onCallEnded = (reason, peerInfo) => {
  console.log(`Call ended: ${reason} by ${peerInfo?.name || 'unknown'}`);
};
client.onRemoteStream = (stream) => {
  document.querySelector('audio')!.srcObject = stream;
};
client.connect();

Flutter/Dart Example

import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';

class SignalingClient {
  late WebSocketChannel _channel;
  RTCPeerConnection? _pc;
  String? _sessionId;
  final String serverUrl;
  final String token;

  Function(String sessionId, String calleeId)? onCallRinging;
  Function(String callerId, String sessionId)? onIncomingCall;
  Function(MediaStream stream)? onRemoteStream;
  Function(String reason)? onCallEnded;

  SignalingClient({required this.serverUrl, required this.token});

  void connect() {
    _channel = WebSocketChannel.connect(Uri.parse('$serverUrl/ws?token=$token'));
    _channel.stream.listen(_handleMessage);
  }

  void _handleMessage(dynamic data) {
    final msg = jsonDecode(data);
    switch (msg['type']) {
      case 'call.ringing':
        _sessionId = msg['payload']['session_id'];
        onCallRinging?.call(_sessionId!, msg['payload']['callee_id']);
        break;
      case 'call.ring':
        _sessionId = msg['payload']['session_id'];
        onIncomingCall?.call(msg['payload']['caller_id'], _sessionId!);
        break;
      case 'call.accepted':
        _sessionId = msg['payload']['session_id'];
        _startWebRTC(isCaller: true);
        break;
      case 'webrtc.offer':
        _handleOffer(msg['payload']['sdp']);
        break;
      case 'webrtc.answer':
        _pc?.setRemoteDescription(RTCSessionDescription(msg['payload']['sdp'], 'answer'));
        break;
      case 'webrtc.ice':
        if (msg['payload']['candidate'] != null) {
          _pc?.addCandidate(RTCIceCandidate(msg['payload']['candidate'], '', 0));
        }
        break;
      case 'call.ended':
        _cleanup();
        onCallEnded?.call(msg['payload']['reason']);
        break;
    }
  }

  void startCall(String calleeId) {
    _send('call.start', {'callee_id': calleeId});
  }

  void acceptCall(String sessionId) {
    _sessionId = sessionId;
    _send('call.accept', {'session_id': sessionId});
    _startWebRTC(isCaller: false);
  }

  void rejectCall(String sessionId) {
    _send('call.reject', {'session_id': sessionId});
  }

  void endCall() {
    if (_sessionId != null) {
      _send('call.end', {'session_id': _sessionId});
      _cleanup();
    }
  }

  Future<void> _startWebRTC({required bool isCaller}) async {
    _pc = await createPeerConnection({
      'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]
    });

    _pc!.onIceCandidate = (candidate) {
      _send('webrtc.ice', {
        'session_id': _sessionId,
        'candidate': candidate.candidate
      });
    };

    _pc!.onTrack = (event) {
      if (event.streams.isNotEmpty) {
        onRemoteStream?.call(event.streams[0]);
      }
    };

    final stream = await navigator.mediaDevices.getUserMedia({'audio': true});
    stream.getTracks().forEach((track) => _pc!.addTrack(track, stream));

    if (isCaller) {
      final offer = await _pc!.createOffer();
      await _pc!.setLocalDescription(offer);
      _send('webrtc.offer', {'session_id': _sessionId, 'sdp': offer.sdp});
    }
  }

  Future<void> _handleOffer(String sdp) async {
    await _pc?.setRemoteDescription(RTCSessionDescription(sdp, 'offer'));
    final answer = await _pc?.createAnswer();
    await _pc?.setLocalDescription(answer!);
    _send('webrtc.answer', {'session_id': _sessionId, 'sdp': answer!.sdp});
  }

  void _send(String type, Map<String, dynamic> payload) {
    _channel.sink.add(jsonEncode({'type': type, 'payload': payload}));
  }

  void _cleanup() {
    _pc?.close();
    _pc = null;
    _sessionId = null;
  }
}

Backend Architecture

Components

  • cmd/server/main.go - Application entry point

    • Initializes Redis, Hub, CallManager, and PubSub
    • Sets up HTTP server with /ws and /health endpoints
    • Handles graceful shutdown
  • internal/auth/jwt.go - JWT authentication

    • Validates tokens using HMAC-SHA256
    • Extracts user ID from hash claim
    • Extracts user profile info (name, username, image_profile) from claims
    • Test token generation for development (with GenerateTokenWithInfo)
  • internal/protocol/messages.go - Protocol definitions

    • All WebSocket message types and structures
    • Call states and error codes
    • UserInfo struct for profile data in call events
  • internal/redis/ - Redis integration

    • client.go - Redis client wrapper
    • session.go - Session and state management with atomic operations
  • internal/notification/ - Push notification backends

    • notification.go - Service interface, RoutingNotifier (dispatches by OS), NoopService
    • fcm.go - Firebase Cloud Messaging v1 via OAuth2 service account
    • apns.go - Apple Push Notification service via .p12 certificate + HTTP/2
  • internal/signaling/ - Call orchestration

    • call.go - Call lifecycle management (start, accept, reject, end); sends push notifications for offline callees
    • pubsub.go - Cross-instance messaging for horizontal scaling
  • internal/ws/ - WebSocket handling

    • handler.go - Hub for connection management and message routing
    • client.go - Individual WebSocket connection handling

Key Design Patterns

  • Stateless servers - All state stored in Redis for horizontal scalability
  • Atomic operations - Lua scripts prevent race conditions
  • Pub/Sub architecture - Cross-instance messaging via Redis channels
  • Connection deduplication - Redis SET NX ensures single connection per user
  • Automatic cleanup - TTLs and disconnect handlers prevent orphaned sessions
  • Heartbeat mechanism - Periodic Redis TTL refresh keeps sessions alive

Technologies

Backend

  • Go 1.24+ with standard library HTTP server
  • gorilla/websocket for WebSocket connections
  • redis/go-redis/v9 for state management
  • golang-jwt/jwt/v5 for authentication
  • rs/zerolog for structured logging
  • golang.org/x/oauth2 for FCM service account authentication
  • golang.org/x/crypto for .p12 certificate decoding (APNs)

Frontend (Example Client)

  • React 18 with TypeScript 5
  • Vite 5 for development and building
  • Native WebRTC API for peer-to-peer media
  • Native WebSocket API for signaling

Infrastructure

  • Redis 7 for state and pub/sub
  • Docker and Docker Compose for deployment

Production Considerations

Security

  • Always use secure JWT secrets in production
  • Consider rate limiting on WebSocket connections
  • Use WSS (WebSocket Secure) in production
  • Implement user authentication in your auth service
  • Validate and sanitize all client inputs

Scaling

  • The server is stateless and can scale horizontally
  • Use a load balancer (e.g., nginx, HAProxy) in front of multiple instances
  • Redis can be clustered for high availability
  • Monitor Redis memory usage and set appropriate TTLs

Monitoring

  • Health check endpoint at /health for load balancer checks
  • Structured JSON logs for aggregation (ELK, Datadog, etc.)
  • Monitor WebSocket connection count
  • Track call success/failure rates
  • Monitor Redis latency and memory

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Go 99.1%
  • Dockerfile 0.9%