Navigation Menu
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:
DRAFTREGISTRATION_OPENREGISTRATION_CLOSEDREADYIN_PROGRESSPAUSEDCOMPLETEDCANCELLED
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:
- WebSocket server is running (
cd backend && npm run ws:dev) - Correct URL in environment variable
- Firewall allows WebSocket connections
- 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:
- Joined correct tournament room
- Event listener registered before event fires
- 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:
- Event listeners are cleaned up
- Socket disconnected on unmount
- 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