Skip to main content
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:
1

Navigate to Organizers Tab

On the tournament page, click the Organizers tab.
2

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>
3

Add Organizer

Click Add next to the user to grant organizer permissions.They will immediately have full tournament control.
4

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:
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

Team Management

Adding Teams

Organizers can add teams in multiple ways:
Add a team directly through the Teams tab:
1

Open Add Team Form

On the Teams tab, find the Add Team card (organizers only).
2

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>
3

Add Roster Players

Add players to the team roster:
interface RosterPlayer {
  player_steam_id: string;
  role?: 'captain' | 'player' | 'substitute';
}
4

Submit Team

Click Add Team. The team is immediately added to the tournament.

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:
1

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>
2

Seeding Strategy

Common seeding approaches:
Seed teams by join time:
teams.sort((a, b) => 
  a.created_at.localeCompare(b.created_at)
).forEach((team, index) => {
  team.seed = index + 1;
});
3

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:
1

Navigate to Bracket

View the tournament bracket on the Overview tab.
2

Click Match

Click any match card that has teams assigned but hasn’t started.
3

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>
4

Set Date and Time

Select when the match should begin.
Scheduled time is used for:
  • Player notifications
  • Match countdown display
  • Automatic check-in timing
5

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:
1

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>
2

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>
3

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>
4

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:
  • Verify all stages are configured correctly
  • Check minimum team count is met
  • Review and finalize team seeding
  • Confirm match options (best-of, map pool, etc.)
  • Set up Discord notifications if desired
  • Schedule first round matches if needed
  • Communicate tournament rules to participants
  • Test with a small trial tournament first
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
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

Build docs developers (and LLMs) love