This guide covers all aspects of managing live tournaments, from team administration to match scheduling and tournament control.
Tournament Administration
Organizer Permissions
Tournament organizers have full control over the tournament:
Tournament Settings
Modify tournament details
Update match options
Configure stages
Manage notifications
Team Management
Add/remove teams
Edit rosters
Assign seeds
Check eligibility
Match Control
Schedule matches
Force match results
Pause/resume matches
Cancel matches
Tournament Control
Start/pause/resume
Open/close registration
Cancel tournament
Delete tournament
Adding Co-Organizers
Delegate tournament management to other users:
Navigate to Organizers Tab
On the tournament page, click the Organizers tab.
Search for User
Enter the user’s Steam name or ID in the search field. < FormField name = "steam_id" >
<FormLabel>Search Player</FormLabel>
<Input
v-model="searchQuery"
placeholder="Steam ID or name"
/>
</ FormField >
Add Organizer
Click Add next to the user to grant organizer permissions. They will immediately have full tournament control.
Remove Organizers
Click Remove next to any organizer to revoke their permissions. The tournament creator cannot be removed as an organizer.
Tournament States
Control tournament flow through state changes:
Registration Control
Tournament Control
Cancellation
Open Registration await updateTournamentStatus (
tournamentId ,
'RegistrationOpen'
);
Allows teams to join
Visible on tournament listing
Organizers can still add teams manually
Close Registration await updateTournamentStatus (
tournamentId ,
'RegistrationClosed'
);
Locks team roster
Prepare for tournament start
Finalize seeding
Start Tournament await updateTournamentStatus (
tournamentId ,
'Live'
);
Generates brackets
Creates initial matches
Cannot be undone
Verify all settings before starting. Tournament configuration cannot be changed after start.
Pause Tournament await updateTournamentStatus (
tournamentId ,
'Paused'
);
Stops new match creation
Existing matches can continue
Resume when ready
Resume Tournament await updateTournamentStatus (
tournamentId ,
'Live'
);
Continues tournament progression
New matches created as needed
Cancel Tournament await updateTournamentStatus (
tournamentId ,
'Cancelled'
);
Stops all progression
Cancels pending matches
Cannot be resumed
Delete Tournament await apolloClient . mutate ({
mutation: gql `
mutation DeleteTournament($id: uuid!) {
deleteTournament(tournament_id: $id) {
success
}
}
` ,
variables: { id: tournamentId }
});
Permanently removes tournament
Deletes all associated data
Cannot be undone
Team Management
Adding Teams
Organizers can add teams in multiple ways:
Add a team directly through the Teams tab:
Open Add Team Form
On the Teams tab, find the Add Team card (organizers only).
Enter Team Details
< FormField name = "team_name" >
<FormLabel>Team Name</FormLabel>
<Input v-model="teamName" />
</ FormField >
< FormField name = "owner_steam_id" >
<FormLabel>Team Captain (Steam ID)</FormLabel>
<Input v-model="ownerSteamId" />
</ FormField >
Add Roster Players
Add players to the team roster: interface RosterPlayer {
player_steam_id : string ;
role ?: 'captain' | 'player' | 'substitute' ;
}
Submit Team
Click Add Team . The team is immediately added to the tournament.
Link an existing team:
Search Teams
Search for teams by name or ID.
Select Team
Click team to import its roster automatically.
Verify Roster
Confirm all players meet tournament requirements.
When registration is open, players can join: < template >
< Sheet v-if = " tournament . can_join " v-model : open = " joinSheetOpen " >
< SheetContent >
< SheetHeader >
< SheetTitle > Join Tournament </ SheetTitle >
< SheetDescription >
Minimum {{ tournament . min_players_per_lineup }} players required
</ SheetDescription >
</ SheetHeader >
< TournamentJoinForm
: tournament = " tournament "
@ close = " joinSheetOpen = false "
/>
</ SheetContent >
</ Sheet >
</ template >
Players must:
Be logged in
Meet minimum player requirements
Have eligible roster (no banned players)
Editing Teams
Modify team details and rosters:
Click the team name to edit: await apolloClient . mutate ({
mutation: gql `
mutation UpdateTeamName($id: uuid!, $name: String!) {
update_tournament_teams_by_pk(
pk_columns: { id: $id }
_set: { name: $name }
) {
id
name
}
}
` ,
variables: { id: teamId , name: newName }
});
Modify team roster: Add Player await apolloClient . mutate ({
mutation: gql `
mutation AddRosterPlayer(
$team_id: uuid!
$player_steam_id: bigint!
) {
insert_tournament_team_roster_one(object: {
tournament_team_id: $team_id
player_steam_id: $player_steam_id
}) {
id
}
}
` ,
variables: { team_id , player_steam_id }
});
Remove Player await apolloClient . mutate ({
mutation: gql `
mutation RemoveRosterPlayer($id: uuid!) {
delete_tournament_team_roster_by_pk(id: $id) {
id
}
}
` ,
variables: { id: rosterItemId }
});
Transfer team ownership: await apolloClient . mutate ({
mutation: gql `
mutation ChangeTeamOwner(
$id: uuid!
$owner_steam_id: bigint!
) {
update_tournament_teams_by_pk(
pk_columns: { id: $id }
_set: { owner_steam_id: $owner_steam_id }
) {
id
owner_steam_id
}
}
` ,
variables: { id: teamId , owner_steam_id }
});
Team Seeding
Control bracket placement through seeding:
Assign Seeds
On the Teams tab, set seed positions (1 = highest seed): < template >
< FormField v-for = " team in teams " : name = " `seed_ ${ team . id } ` " >
< FormLabel > {{ team . name }} </ FormLabel >
< Input
type = "number"
: model-value = " team . seed "
@ update : model-value = " updateSeed ( team . id , $event ) "
/>
</ FormField >
</ template >
Seeding Strategy
Common seeding approaches: Registration Order
Skill-Based
Manual
Seed teams by join time: teams . sort (( a , b ) =>
a . created_at . localeCompare ( b . created_at )
). forEach (( team , index ) => {
team . seed = index + 1 ;
});
Seed by team rating/ELO: teams . sort (( a , b ) =>
b . average_elo - a . average_elo
). forEach (( team , index ) => {
team . seed = index + 1 ;
});
Organizers manually assign based on:
Past performance
Known team strength
Regional rankings
Qualifiers/trials
Seed Impact
Seeding affects:
Initial bracket placement
Bye assignments (highest seeds)
Swiss initial pairings
Group distribution
Removing Teams
Teams can only be removed before tournament starts. After start, teams cannot be removed to maintain bracket integrity.
async function removeTeam ( teamId : string ) {
if ( tournament . status === 'Live' ) {
throw new Error ( 'Cannot remove teams after tournament starts' );
}
await apolloClient . mutate ({
mutation: gql `
mutation RemoveTeam($id: uuid!) {
delete_tournament_teams_by_pk(id: $id) {
id
}
}
` ,
variables: { id: teamId }
});
}
Match Scheduling
Organizers can schedule when matches begin:
Navigate to Bracket
View the tournament bracket on the Overview tab.
Click Match
Click any match card that has teams assigned but hasn’t started.
Open Schedule Dialog
The scheduling dialog opens: < template >
< Dialog v-model : open = " scheduleDialogOpen " >
< DialogContent >
< DialogHeader >
< DialogTitle > Schedule Match </ DialogTitle >
< DialogDescription >
{{ bracket . team_1 ?. name }} vs {{ bracket . team_2 ?. name }}
</ DialogDescription >
</ DialogHeader >
< FormField name = "scheduled_at" >
< FormLabel > Match Start Time </ FormLabel >
< div class = "flex gap-2" >
< Popover >
< PopoverTrigger as-child >
< Button variant = "outline" >
< CalendarIcon class = "mr-2 h-4 w-4" />
{{ scheduleDate || 'Pick a date' }}
</ Button >
</ PopoverTrigger >
< PopoverContent >
< Calendar v-model = " scheduleDate " />
</ PopoverContent >
</ Popover >
< Input
type = "time"
v-model = " scheduleTime "
style = " color-scheme : dark "
/>
</ div >
</ FormField >
< DialogFooter >
< Button @ click = " saveSchedule " > Schedule Match </ Button >
</ DialogFooter >
</ DialogContent >
</ Dialog >
</ template >
Set Date and Time
Select when the match should begin. Scheduled time is used for:
Player notifications
Match countdown display
Automatic check-in timing
Save Schedule
Match is updated with schedule: await apolloClient . mutate ({
mutation: gql `
mutation ScheduleMatch(
$bracket_id: uuid!
$scheduled_at: timestamptz!
) {
update_tournament_brackets_by_pk(
pk_columns: { id: $bracket_id }
_set: { scheduled_at: $scheduled_at }
) {
id
scheduled_at
}
}
` ,
variables: { bracket_id , scheduled_at }
});
Bulk Scheduling
Schedule multiple matches efficiently:
interface BulkSchedule {
bracket_id : string ;
scheduled_at : string ;
}
async function bulkScheduleMatches ( schedules : BulkSchedule []) {
const updates = schedules . map (({ bracket_id , scheduled_at }) => ({
where: { id: { _eq: bracket_id } },
_set: { scheduled_at }
}));
await apolloClient . mutate ({
mutation: gql `
mutation BulkSchedule($updates: [tournament_brackets_updates!]!) {
update_tournament_brackets_many(updates: $updates) {
affected_rows
}
}
` ,
variables: { updates }
});
}
// Example: Schedule all Round 1 matches 30 minutes apart
const round1Matches = brackets . filter ( b => b . round === 1 );
const startTime = new Date ( '2024-01-20T18:00:00Z' );
const schedules = round1Matches . map (( bracket , index ) => ({
bracket_id: bracket . id ,
scheduled_at: new Date (
startTime . getTime () + index * 30 * 60 * 1000
). toISOString ()
}));
await bulkScheduleMatches ( schedules );
Tournament Filtering
The tournament management page includes powerful filtering:
< template >
< Card class = "p-4 mb-4" >
< div class = "grid grid-cols-1 md:grid-cols-3 gap-4" >
<!-- Tournament ID Search -->
< FormField name = "tournamentId" >
< FormLabel > Search by ID </ FormLabel >
< Input
v-model = " form . values . tournamentId "
@ update : model-value = " onFilterChange "
placeholder = "Enter tournament ID"
/>
</ FormField >
<!-- Tournament Name Search -->
< FormField name = "tournamentName" >
< FormLabel > Search by Name </ FormLabel >
< Input
v-model = " form . values . tournamentName "
@ update : model-value = " onFilterChange "
placeholder = "Enter tournament name"
/>
</ FormField >
<!-- Status Filter -->
< FormField name = "statuses" >
< FormLabel > Filter by Status </ FormLabel >
< Select
v-model = " form . values . statuses "
@ update : model-value = " onStatusChange "
multiple
>
< SelectTrigger >
< SelectValue placeholder = "Select statuses" />
</ SelectTrigger >
< SelectContent >
< SelectItem
v-for = " status in tournamentStatusOptions "
: value = " status . value "
>
{{ status . label }}
</ SelectItem >
</ SelectContent >
</ Select >
</ FormField >
</ div >
<!-- Additional Filters -->
< div class = "flex items-center justify-between mt-4" >
< div class = "flex items-center space-x-2" >
< Switch v-model = " showOnlyMyTournaments " />
< Label > Only show my tournaments </ Label >
</ div >
< Button variant = "outline" @ click = " resetFilters " >
Reset Filters
</ Button >
</ div >
</ Card >
</ template >
Filter Implementation
function getWhereClause () {
const filterConditions : any = {};
// ID filter
if ( form . values . tournamentId ?. trim ()) {
const tournamentId = form . values . tournamentId . trim ();
if ( validateUUID ( tournamentId )) {
filterConditions . id = { _eq: tournamentId };
} else {
// Return no results for invalid UUID
return { id: { _eq: '00000000-0000-0000-0000-000000000000' } };
}
}
// Name filter (case-insensitive partial match)
if ( form . values . tournamentName ?. trim ()) {
filterConditions . name = {
_ilike: `% ${ form . values . tournamentName . trim () } %`
};
}
// Status filter
if ( form . values . statuses ?. length > 0 ) {
filterConditions . status = {
_in: form . values . statuses
};
}
// My tournaments only
if ( showOnlyMyTournaments ) {
filterConditions . is_organizer = { _eq: true };
}
return filterConditions ;
}
Sorting
< template >
< div class = "flex items-center space-x-4" >
< Select v-model = " sortField " @ update : model-value = " onSortChange " >
< SelectTrigger class = "w-40" >
< SelectValue />
</ SelectTrigger >
< SelectContent >
< SelectItem value = "created_at" > Created At </ SelectItem >
< SelectItem value = "start" > Start Time </ SelectItem >
</ SelectContent >
</ Select >
< Button
variant = "outline"
@ click = " toggleSortDirection "
class = "flex items-center space-x-1"
>
< ArrowUpIcon v-if = " sortDirection === 'desc' " class = "w-4 h-4" />
< ArrowDownIcon v-else class = "w-4 h-4" />
< span > {{ sortDirection === 'desc' ? 'Newest First' : 'Oldest First' }} </ span >
</ Button >
</ div >
</ template >
< script >
function getSortOrder () {
const orderBy = sortDirection === 'desc' ? order_by . desc : order_by . asc ;
switch ( sortField ) {
case 'created_at' :
return { created_at: orderBy };
case 'start' :
return { start: orderBy };
default :
return { created_at: orderBy };
}
}
</ script >
Discord Notifications
Configure automated Discord notifications:
Enable Notifications
Toggle Discord notifications in tournament settings: < FormField name = "discord_notifications_enabled" >
<Card class="cursor-pointer" @click="handleChange(!value)">
<div class="flex justify-between items-center p-4">
<FormLabel>Discord Notifications</FormLabel>
<Switch :model-value="value" />
</div>
<FormDescription class="px-4 pb-4">
Send match notifications to Discord webhook
</FormDescription>
</Card>
</ FormField >
Configure Webhook
On the Notifications tab, add webhook details: < FormField name = "discord_webhook" >
<FormLabel>Discord Webhook URL</FormLabel>
<Input
v-model="webhookUrl"
placeholder="https://discord.com/api/webhooks/..."
/>
<FormDescription>
Create a webhook in your Discord server settings
</FormDescription>
</ FormField >
< FormField name = "discord_role_id" >
<FormLabel>Role to Ping (Optional)</FormLabel>
<Input
v-model="roleId"
placeholder="123456789012345678"
/>
<FormDescription>
Discord role ID to mention in notifications
</FormDescription>
</ FormField >
Select Events
Choose which events trigger notifications: < div class = "space-y-2" >
<FormField name="discord_notify_Scheduled">
<div class="flex items-center justify-between">
<FormLabel>Match Scheduled</FormLabel>
<Switch v-model="form.values.discord_notify_Scheduled" />
</ div >
< / FormField >
<FormField name="discord_notify_WaitingForCheckIn">
<div class="flex items-center justify-between">
<FormLabel>Waiting for Check-in</FormLabel>
<Switch v-model="form.values.discord_notify_WaitingForCheckIn" />
</div>
</ FormField >
< FormField name = "discord_notify_PickingPlayers" >
<div class="flex items-center justify-between">
<FormLabel>Picking Players</FormLabel>
<Switch v-model="form.values.discord_notify_PickingPlayers" />
</div>
</ FormField >
< FormField name = "discord_notify_Veto" >
<div class="flex items-center justify-between">
<FormLabel>Map Veto Phase</FormLabel>
<Switch v-model="form.values.discord_notify_Veto" />
</div>
</ FormField >
< FormField name = "discord_notify_Live" >
<div class="flex items-center justify-between">
<FormLabel>Match Live</FormLabel>
<Switch v-model="form.values.discord_notify_Live" />
</div>
</ FormField >
< FormField name = "discord_notify_Finished" >
<div class="flex items-center justify-between">
<FormLabel>Match Finished</FormLabel>
<Switch v-model="form.values.discord_notify_Finished" />
</div>
</ FormField >
< / div >
Test Notification
Send a test message to verify configuration.
Notification Payload
Discord notifications are sent as embeds:
interface DiscordNotification {
content ?: string ; // Role ping if configured
embeds : [{
title : string ;
description : string ;
color : number ;
fields : {
name : string ;
value : string ;
inline ?: boolean ;
}[];
timestamp : string ;
url ?: string ;
}];
}
// Example: Match Live notification
const notification : DiscordNotification = {
content: roleId ? `<@& ${ roleId } >` : undefined ,
embeds: [{
title: '🔴 Match is Live!' ,
description: ` ${ team1 . name } vs ${ team2 . name } ` ,
color: 0xFF0000 ,
fields: [
{
name: 'Tournament' ,
value: tournament . name ,
inline: true
},
{
name: 'Format' ,
value: `Best of ${ match . options . best_of } ` ,
inline: true
},
{
name: 'Round' ,
value: getRoundLabel ( bracket . round ),
inline: true
}
],
timestamp: new Date (). toISOString (),
url: `https://5stack.gg/matches/ ${ match . id } `
}]
};
Best Practices
Before starting your tournament:
While tournament is running:
Monitor match progression regularly
Be available for dispute resolution
Watch for technical issues (server problems, etc.)
Communicate delays or changes quickly
Keep Discord/communication channels active
Have backup organizers available
Be prepared to pause if major issues arise
Best practices for teams:
Verify roster eligibility before start
Set clear roster lock deadlines
Use seeding consistently and fairly
Document seeding criteria publicly
Allow reasonable time for team formation
Have clear substitution rules
Communicate roster requirements clearly
Scheduling recommendations:
Allow 90-120 minutes per BO3
Schedule stagger to avoid server shortages
Give teams 15-30 minute warning
Account for check-in time (5-10 minutes)
Build in buffer time for delays
Schedule finals for peak viewership
Avoid scheduling conflicts with major events
Troubleshooting
Symptoms : Tournament starts but no brackets appearSolutions :
Verify minimum team count is met
Check that at least one stage is configured
Ensure team count matches stage requirements
Review stage configuration for errors
Try pausing and resuming tournament
Symptoms : Completed matches don’t advance bracketSolutions :
Verify match actually finished (not forfeit/cancelled)
Check for tied matches (requires resolution)
Ensure all matches in round are complete
Look for bracket generation errors in logs
Contact support if issue persists
Symptoms : Registration open but teams can’t joinSolutions :
Confirm tournament status is “Registration Open”
Check team meets minimum player requirements
Verify no banned players on roster
Ensure tournament hasn’t reached max teams
Check player eligibility requirements
Discord notifications not working
Symptoms : Webhook configured but no messagesSolutions :
Verify webhook URL is correct and active
Check Discord server permissions
Test webhook with external tool first
Confirm specific events are enabled
Review role ID format if using pings
Check webhook isn’t rate-limited
GraphQL Operations
Key mutations for tournament management:
// Update tournament status
const UPDATE_TOURNAMENT_STATUS = gql `
mutation UpdateTournamentStatus(
$id: uuid!
$status: e_tournament_status_enum!
) {
update_tournaments_by_pk(
pk_columns: { id: $id }
_set: { status: $status }
) {
id
status
}
}
` ;
// Add tournament team
const ADD_TOURNAMENT_TEAM = gql `
mutation AddTeam($object: tournament_teams_insert_input!) {
insert_tournament_teams_one(object: $object) {
id
name
seed
}
}
` ;
// Update team seed
const UPDATE_TEAM_SEED = gql `
mutation UpdateTeamSeed($id: uuid!, $seed: Int) {
update_tournament_teams_by_pk(
pk_columns: { id: $id }
_set: { seed: $seed }
) {
id
seed
}
}
` ;
// Schedule bracket
const SCHEDULE_BRACKET = gql `
mutation ScheduleBracket(
$bracket_id: uuid!
$scheduled_at: timestamptz!
) {
update_tournament_brackets_by_pk(
pk_columns: { id: $bracket_id }
_set: { scheduled_at: $scheduled_at }
) {
id
scheduled_at
}
}
` ;
// Delete tournament
const DELETE_TOURNAMENT = gql `
mutation DeleteTournament($tournament_id: uuid!) {
deleteTournament(tournament_id: $tournament_id) {
success
}
}
` ;
Next Steps
Creating Tournaments Learn how to create and configure tournaments
Tournament Formats Understand different tournament formats
Bracket Visualization Master bracket navigation and display
Match Management Learn about match administration