WebSocket Events Reference

Dutchie's Brackets uses Socket.IO for real-time updates during tournaments. This document covers all available WebSocket events and how to use them.

Connection

WebSocket Server

URL:

http://localhost:3002 (development)
https://your-domain.com:3002 (production)

Configuration:

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

const socket = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:3002', {
  transports: ['websocket', 'polling'],
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
});

Connection Events

connect

Fired when connection is established.

socket.on('connect', () => {
  console.log('Connected to WebSocket server');
  console.log('Socket ID:', socket.id);
});

disconnect

Fired when connection is lost.

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

  if (reason === 'io server disconnect') {
    // Server forcibly disconnected, manually reconnect
    socket.connect();
  }
  // else automatic reconnection will occur
});

reconnect

Fired when reconnection succeeds.

socket.on('reconnect', (attemptNumber) => {
  console.log('Reconnected after', attemptNumber, 'attempts');
  // Re-join tournament rooms
});

Room Management

Joining Tournament Room

Event: join_tournament

Emit:

socket.emit('join_tournament', { tournamentId: 'clx123abc' });

Response: tournament_joined

socket.on('tournament_joined', (data) => {
  console.log('Joined tournament:', data.tournamentId);
  console.log('Current participants:', data.participantCount);
});

Payload:

{
  "tournamentId": "clx123abc",
  "participantCount": 12,
  "status": "IN_PROGRESS"
}

Leaving Tournament Room

Event: leave_tournament

Emit:

socket.emit('leave_tournament', { tournamentId: 'clx123abc' });

Response: tournament_left

socket.on('tournament_left', (data) => {
  console.log('Left tournament:', data.tournamentId);
});

Tournament Events

participant_joined

Fired when a new participant joins the tournament.

Listen:

socket.on('participant_joined', (data) => {
  console.log('New participant:', data.displayName);
  // Update participant list UI
});

Payload:

{
  "id": "clp789def",
  "displayName": "Alice Johnson",
  "participantType": "FULL_ACCOUNT",
  "tournamentId": "clx123abc"
}

Use Cases:

  • Update participant count
  • Add participant to list
  • Show notification to organizer

bracket_generated

Fired when tournament bracket is generated.

Listen:

socket.on('bracket_generated', (data) => {
  console.log('Bracket generated');
  console.log('Total matches:', data.matchCount);
  // Redirect to bracket view
});

Payload:

{
  "tournamentId": "clx123abc",
  "roundCount": 4,
  "matchCount": 15,
  "format": "DOUBLE_ELIMINATION"
}

Use Cases:

  • Redirect participants to bracket page
  • Enable "Start Tournament" button
  • Display bracket structure

match_updated

Fired when match details change (scores, status, etc.).

Listen:

socket.on('match_updated', (data) => {
  console.log('Match updated:', data.matchId);
  // Update specific match in UI
});

Payload:

{
  "tournamentId": "clx123abc",
  "matchId": "clm456",
  "roundId": "clr123",
  "player1Score": 5,
  "player2Score": 3,
  "status": "COMPLETED",
  "winnerId": "clp123xyz"
}

Use Cases:

  • Update live scores
  • Show match in progress
  • Highlight completed matches

match_completed

Fired when a match is finished and results are entered.

Listen:

socket.on('match_completed', (data) => {
  console.log('Match completed');
  console.log('Winner:', data.winnerName);
  // Update bracket, trigger animations
});

Payload:

{
  "tournamentId": "clx123abc",
  "matchId": "clm456",
  "roundId": "clr123",
  "roundNumber": 1,
  "winnerId": "clp123xyz",
  "winnerName": "Alice Johnson",
  "loserId": "clp456abc",
  "loserName": "Bob Smith",
  "player1Score": 5,
  "player2Score": 3,
  "completedAt": "2025-07-15T10:45:00Z"
}

Use Cases:

  • Advance winner in bracket
  • Move loser to losers bracket
  • Update participant status
  • Show celebration animation

round_completed

Fired when all matches in a round are finished.

Listen:

socket.on('round_completed', (data) => {
  console.log('Round', data.roundNumber, 'completed');
  // Announce next round
});

Payload:

{
  "tournamentId": "clx123abc",
  "roundId": "clr123",
  "roundNumber": 1,
  "roundName": "Round 1",
  "nextRoundId": "clr456",
  "nextRoundNumber": 2
}

Use Cases:

  • Display "Round Complete" message
  • Prepare next round matches
  • Give participants break time

tournament_status_changed

Fired when tournament status changes.

Listen:

socket.on('tournament_status_changed', (data) => {
  console.log('Tournament status:', data.newStatus);
  // Update UI based on status
});

Payload:

{
  "tournamentId": "clx123abc",
  "oldStatus": "READY",
  "newStatus": "IN_PROGRESS",
  "changedAt": "2025-07-15T10:00:00Z"
}

Status Values:

  • DRAFT
  • REGISTRATION_OPEN
  • REGISTRATION_CLOSED
  • READY
  • IN_PROGRESS
  • PAUSED
  • COMPLETED
  • CANCELLED

Use Cases:

  • Enable/disable features
  • Show status badge
  • Notify participants

tournament_completed

Fired when tournament finishes (all matches complete).

Listen:

socket.on('tournament_completed', (data) => {
  console.log('Tournament complete!');
  console.log('Champion:', data.championName);
  // Show results, confetti animation
});

Payload:

{
  "tournamentId": "clx123abc",
  "championId": "clp123xyz",
  "championName": "Alice Johnson",
  "runnerUpId": "clp456abc",
  "runnerUpName": "Bob Smith",
  "completedAt": "2025-07-15T16:30:00Z",
  "totalMatches": 15,
  "duration": "6h 30m"
}

Use Cases:

  • Display winner
  • Show final results
  • Trigger celebration effects
  • Enable result sharing

Error Events

error

Fired when an error occurs.

Listen:

socket.on('error', (error) => {
  console.error('WebSocket error:', error);
  // Show error to user
});

Payload:

{
  "message": "Failed to join tournament",
  "code": "TOURNAMENT_NOT_FOUND",
  "details": {}
}

React Hook Example

useWebSocket Hook

import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';

interface WebSocketOptions {
  tournamentId?: string;
  autoConnect?: boolean;
}

export function useWebSocket({ tournamentId, autoConnect = true }: WebSocketOptions) {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [participants, setParticipants] = useState([]);

  useEffect(() => {
    if (!autoConnect) return;

    const socketInstance = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:3002', {
      transports: ['websocket', 'polling'],
    });

    setSocket(socketInstance);

    socketInstance.on('connect', () => {
      console.log('WebSocket connected');
      setIsConnected(true);

      if (tournamentId) {
        socketInstance.emit('join_tournament', { tournamentId });
      }
    });

    socketInstance.on('disconnect', () => {
      console.log('WebSocket disconnected');
      setIsConnected(false);
    });

    socketInstance.on('participant_joined', (data) => {
      console.log('New participant:', data);
      setParticipants((prev) => [...prev, data]);
    });

    socketInstance.on('match_completed', (data) => {
      console.log('Match completed:', data);
      // Handle match completion
    });

    socketInstance.on('tournament_completed', (data) => {
      console.log('Tournament completed:', data);
      // Handle tournament completion
    });

    return () => {
      if (tournamentId) {
        socketInstance.emit('leave_tournament', { tournamentId });
      }
      socketInstance.disconnect();
    };
  }, [tournamentId, autoConnect]);

  return {
    socket,
    isConnected,
    participants,
  };
}

Usage:

function TournamentPage({ tournamentId }: { tournamentId: string }) {
  const { socket, isConnected, participants } = useWebSocket({ tournamentId });

  return (
    <div>
      <ConnectionStatus connected={isConnected} />
      <ParticipantList participants={participants} />
    </div>
  );
}

Broadcasting Events (Server-Side)

From API Routes

import { broadcastParticipantJoined } from '@/lib/websocket-broadcast';

// After participant joins
await broadcastParticipantJoined(tournamentId, {
  id: participant.id,
  displayName: participant.displayName,
  participantType: participant.participantType,
});

Broadcast Functions

Located in /src/lib/websocket-broadcast.ts:

// Participant joined
export async function broadcastParticipantJoined(
  tournamentId: string,
  participant: ParticipantData
): Promise<void>;

// Match completed
export async function broadcastMatchCompleted(
  tournamentId: string,
  matchData: MatchData
): Promise<void>;

// Tournament status changed
export async function broadcastTournamentStatusChanged(
  tournamentId: string,
  oldStatus: string,
  newStatus: string
): Promise<void>;

Best Practices

Connection Management

Best Practice: Connect once per app

// Use singleton pattern or context provider
const socket = io(WS_URL);

Don't create multiple connections

// Bad: Creates new connection on every render
useEffect(() => {
  const socket = io(WS_URL);
  // ...
}, []); // Missing cleanup

Room Management

Best Practice: Join tournament room on mount

useEffect(() => {
  socket.emit('join_tournament', { tournamentId });

  return () => {
    socket.emit('leave_tournament', { tournamentId });
  };
}, [tournamentId]);

Error Handling

Best Practice: Handle connection errors gracefully

socket.on('connect_error', (error) => {
  console.error('Connection failed:', error);
  // Show offline indicator
  setConnectionStatus('offline');
});

socket.on('reconnect_failed', () => {
  // Show persistent error message
  showError('Unable to connect to live updates');
});

Event Cleanup

Best Practice: Remove event listeners on unmount

useEffect(() => {
  const handleMatchUpdate = (data) => {
    // Handle event
  };

  socket.on('match_updated', handleMatchUpdate);

  return () => {
    socket.off('match_updated', handleMatchUpdate);
  };
}, []);

Troubleshooting

Connection Issues

Problem: WebSocket won't connect

Check:

  1. WebSocket server is running (cd backend && npm run ws:dev)
  2. Correct URL in environment variable
  3. Firewall allows WebSocket connections
  4. CORS configured properly

Debug:

socket.on('connect_error', (error) => {
  console.log('Connection error:', error);
  console.log('Transport:', socket.io.engine.transport.name);
});

Events Not Received

Problem: Not receiving expected events

Check:

  1. Joined correct tournament room
  2. Event listener registered before event fires
  3. Server is broadcasting to correct room

Debug:

// Log all events
socket.onAny((eventName, ...args) => {
  console.log('Event received:', eventName, args);
});

Memory Leaks

Problem: App slowing down over time

Check:

  1. Event listeners are cleaned up
  2. Socket disconnected on unmount
  3. Not creating multiple socket instances

Fix:

useEffect(() => {
  // Register listeners

  return () => {
    // CRITICAL: Clean up
    socket.off('match_updated');
    socket.off('participant_joined');
    socket.disconnect();
  };
}, []);

Next Steps


Questions? See FAQ or GitHub Issues.

Was this page helpful?

Help us improve our documentation. Found a typo or have a suggestion?

Report an Issue