Build a mobile esports platform with Firestore collections for tournaments, matches, and teams. Create tournament brackets using a Custom Widget that renders an elimination tree from match data. Teams register for tournaments, both teams submit scores after each match, and an admin confirms results before advancing the bracket. Real-time Firestore listeners update scores live for spectators. A platform leaderboard ranks teams by wins across all tournaments.
Building a Mobile Esports Tournament Platform in FlutterFlow
Competitive mobile gaming needs structured tournament management with brackets, team rosters, score reporting, and live updates. This tutorial builds an esports platform where organizers create tournaments, teams register and manage rosters, matches follow elimination brackets with verified score submissions, and spectators watch live score updates.
Prerequisites
- A FlutterFlow project with Firestore and authentication configured
- Familiarity with FlutterFlow Custom Widgets (Dart code)
- Basic understanding of single-elimination tournament bracket structure
Step-by-step guide
Design the Firestore data model for tournaments, matches, and teams
Design the Firestore data model for tournaments, matches, and teams
Create a tournaments collection with fields: name (String), game (String), format (String: 'single_elimination', 'round_robin', 'swiss'), maxTeams (Integer), startDate (Timestamp), prizePool (String), status (String: 'registration', 'in_progress', 'completed'), organizedBy (String). Under each tournament, add a matches subcollection: team1Id (String), team2Id (String), team1Name (String), team2Name (String), round (Integer), matchNumber (Integer), score1 (Integer, nullable), score2 (Integer, nullable), team1Submitted (Map: {score1, score2}), team2Submitted (Map: {score1, score2}), winnerId (String, nullable), status (String: 'scheduled', 'live', 'completed'), scheduledTime (Timestamp). Create a teams collection: name (String), captainId (String), memberIds (String Array), logoUrl (String), wins (Integer), losses (Integer).
Expected result: Firestore has tournaments with matches subcollection and a teams collection ready for bracket-based competitions.
Build the tournament bracket visualization as a Custom Widget
Build the tournament bracket visualization as a Custom Widget
Create a Custom Widget named TournamentBracket that receives a list of match documents as a parameter. The widget renders a horizontal tree layout: Round 1 matches on the left, each pair of winners connecting to a Round 2 match to the right, and so on until the final. Each match node is a Container showing team1Name vs team2Name with scores. Highlight the winner in green and the loser in grey. Connect matches with painted lines using CustomPainter. For an 8-team single elimination bracket, you have 4 Round 1 matches, 2 Round 2 matches, and 1 final. The widget calculates vertical spacing to align match connectors properly.
1// Custom Widget: TournamentBracket (simplified)2import 'package:flutter/material.dart';34class TournamentBracket extends StatelessWidget {5 final List<Map<String, dynamic>> matches;6 const TournamentBracket({super.key, required this.matches});78 @override9 Widget build(BuildContext context) {10 final rounds = <int, List<Map<String, dynamic>>>{};11 for (final m in matches) {12 final r = m['round'] as int;13 rounds.putIfAbsent(r, () => []).add(m);14 }15 final maxRound = rounds.keys.reduce(16 (a, b) => a > b ? a : b);17 return SingleChildScrollView(18 scrollDirection: Axis.horizontal,19 child: Row(20 crossAxisAlignment: CrossAxisAlignment.center,21 children: List.generate(maxRound, (i) {22 final round = i + 1;23 final roundMatches = rounds[round] ?? [];24 roundMatches.sort((a, b) =>25 (a['matchNumber'] as int)26 .compareTo(b['matchNumber'] as int));27 return Padding(28 padding: const EdgeInsets.symmetric(29 horizontal: 16),30 child: Column(31 mainAxisAlignment:32 MainAxisAlignment.spaceEvenly,33 children: roundMatches.map((m) {34 return _MatchCard(match: m);35 }).toList(),36 ),37 );38 }),39 ),40 );41 }42}4344class _MatchCard extends StatelessWidget {45 final Map<String, dynamic> match;46 const _MatchCard({required this.match});4748 @override49 Widget build(BuildContext context) {50 final winner = match['winnerId'];51 return Container(52 width: 200, margin: const EdgeInsets.all(8),53 decoration: BoxDecoration(54 border: Border.all(color: Colors.grey),55 borderRadius: BorderRadius.circular(8)),56 child: Column(children: [57 _teamRow(match['team1Name'],58 match['score1'], match['team1Id'] == winner),59 const Divider(height: 1),60 _teamRow(match['team2Name'],61 match['score2'], match['team2Id'] == winner),62 ]),63 );64 }6566 Widget _teamRow(String? name, int? score, bool won) {67 return Container(68 color: won ? Colors.green.shade50 : null,69 padding: const EdgeInsets.all(8),70 child: Row(children: [71 Expanded(child: Text(name ?? 'TBD',72 style: TextStyle(73 fontWeight: won ? FontWeight.bold74 : FontWeight.normal))),75 Text('${score ?? "-"}',76 style: const TextStyle(fontSize: 16)),77 ]),78 );79 }80}Expected result: A horizontally scrollable bracket widget displays all rounds with match cards showing teams, scores, and highlighted winners.
Implement team registration and roster management
Implement team registration and roster management
Create a TeamManagementPage where the team captain manages their roster. Display the team name, logo (editable Image with upload), and a ListView of memberIds resolved to user names. Add an Invite Member button that searches users by name and adds their UID to memberIds. For tournament registration, on the TournamentDetailPage add a Register Team button visible when status is 'registration' and the team has not already registered. On tap, create a registration document and add the team to the tournament's registered teams. Show a registration count (e.g., 6/8 teams registered) with a progress indicator.
Expected result: Team captains manage rosters and register for tournaments. The tournament page shows registration progress toward the maximum team count.
Build the dual-submission score reporting system
Build the dual-submission score reporting system
On the MatchDetailPage, each team captain sees a Submit Score form with two number inputs (your score and opponent score). On submit, write the scores to team1Submitted or team2Submitted (based on which team the user captains) on the match document. After both teams submit, a Cloud Function compares the submissions. If they match, set score1, score2, winnerId, and status to 'completed'. If they conflict, set status to 'disputed' and notify the tournament admin. The admin sees a Dispute Resolution panel showing both submissions and can manually set the final score. This prevents any single team from fabricating results.
Expected result: Both teams submit scores independently. Matching scores auto-confirm. Conflicting scores flag a dispute for admin resolution.
Add real-time live score updates for spectators
Add real-time live score updates for spectators
On the TournamentBracketPage, set the Backend Query on matches to real-time (disable Single Time Query). When a match score is updated, the bracket widget rebuilds instantly showing new scores and advancing winners. Add a LiveMatchesPage that queries matches where status is 'live', showing a ListView of currently active matches with team names, current scores, and a pulsing red Live indicator. Spectators can tap any live match to see the MatchDetailPage with real-time score updates. Use a Container with a red dot and pulsing animation (Lottie or simple AnimatedOpacity) for the live indicator.
Expected result: Spectators see live score updates across all matches in real-time without refreshing. Active matches display a Live indicator.
Create the platform leaderboard ranking teams across tournaments
Create the platform leaderboard ranking teams across tournaments
Build a LeaderboardPage with a ListView querying teams ordered by wins descending. Each row shows: rank number, team logo, team name, wins count, losses count, and win rate percentage. Add a TabBar to filter by game (All Games, Game A, Game B). For game-specific rankings, use a Cloud Function that tallies wins per game from completed matches and stores them in a team_game_stats subcollection. Highlight the top 3 teams with gold, silver, and bronze accent colors. Add a TournamentHistoryPage accessible from each team showing all tournaments they participated in with their finishing position.
Expected result: A global leaderboard ranks teams by wins with game-specific filtering and highlighted top-3 positions.
Complete working example
1FIRESTORE DATA MODEL:2 tournaments/{tournamentId}3 name: String4 game: String5 format: 'single_elimination' | 'round_robin' | 'swiss'6 maxTeams: Integer7 startDate: Timestamp8 prizePool: String9 status: 'registration' | 'in_progress' | 'completed'10 └── matches/{matchId}11 team1Id: String12 team2Id: String13 team1Name: String14 team2Name: String15 round: Integer16 matchNumber: Integer17 score1: Integer (nullable)18 score2: Integer (nullable)19 team1Submitted: { score1: int, score2: int }20 team2Submitted: { score1: int, score2: int }21 winnerId: String (nullable)22 status: 'scheduled' | 'live' | 'completed' | 'disputed'23 scheduledTime: Timestamp2425 teams/{teamId}26 name: String27 captainId: String28 memberIds: [String]29 logoUrl: String30 wins: Integer31 losses: Integer3233PAGE: TournamentBracketPage (Route: tournamentId)34 Column35 ├── Text (tournament name + game)36 ├── Row (status badge + teams count + prize pool)37 └── Custom Widget: TournamentBracket38 Backend Query: matches (real-time), all rounds39 Renders horizontal tree: Round 1 → Round 2 → Final4041PAGE: MatchDetailPage (Route: matchId, tournamentId)42 Column43 ├── Container (team1 vs team2 with logos)44 ├── Text (score1 : score2, large font)45 ├── Live indicator (if status == 'live')46 └── Submit Score form (for team captains only)47 Two number inputs + Submit button4849PAGE: LeaderboardPage50 Column51 ├── TabBar (All Games | Game filters)52 └── ListView (teams ordered by wins desc)53 Row: rank + logo + name + wins + losses + win%5455SCORE VERIFICATION FLOW:56 1. Team1 captain submits → team1Submitted written57 2. Team2 captain submits → team2Submitted written58 3. Cloud Function compares submissions59 4. Match → scores confirmed OR disputed60 5. Winner advances to next round matchCommon mistakes when creating a Mobile Esports Platform with Live Competitions in FlutterFlow
Why it's a problem: Auto-advancing the bracket based on a single team's score submission
How to avoid: Require both teams to submit matching scores. If scores conflict, flag the match as disputed for admin resolution before advancing.
Why it's a problem: Not using real-time queries for live match scores
How to avoid: Disable Single Time Query on all match-related Backend Queries so Firestore real-time listeners push score updates instantly.
Why it's a problem: Hardcoding bracket positions instead of calculating from match data
How to avoid: Calculate bracket layout dynamically from the round and matchNumber fields. The widget should handle any number of rounds automatically.
Best practices
- Require dual score submission from both teams before confirming match results
- Use real-time Firestore listeners for all match and bracket displays
- Build the bracket widget to dynamically handle any number of rounds and teams
- Add a dispute resolution flow for conflicting score submissions
- Show a live indicator on active matches for spectator engagement
- Maintain a global leaderboard ranking teams across all tournaments
- Store team1Name and team2Name on match documents to avoid extra reads when rendering brackets
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a mobile esports tournament platform in FlutterFlow with single-elimination brackets, team registration, dual score submission with dispute handling, live match updates, and a global leaderboard. Show me the Firestore data model, Custom Widget for bracket visualization, score verification Cloud Function, and page layouts.
Create a tournament detail page with the tournament name at top, a horizontal scrollable area in the middle for bracket display, and a list of upcoming matches at the bottom with team names and scheduled times.
Frequently asked questions
Can I support different tournament formats like round-robin or swiss?
Yes. The format field on tournaments supports multiple types. For round-robin, generate matches so every team plays every other team. For swiss, create matches round by round pairing teams with similar records. The bracket widget adapts to display different formats.
How do I generate the bracket matches automatically?
Create a Cloud Function triggered when tournament status changes to 'in_progress'. For single elimination with 8 teams, generate 4 Round 1 matches by seeding or random draw, 2 Round 2 match placeholders, and 1 final. Advance winners by updating the next round match documents.
Can spectators place predictions or bets on matches?
You can add a predictions feature where users select winners before matches start. Store predictions in a user_predictions subcollection and calculate accuracy scores. Actual betting requires gambling license compliance which varies by jurisdiction.
How do I handle teams that do not show up for their match?
Add a 'forfeit' status option for matches. If a team does not submit scores within a set time after the scheduled start, the admin can forfeit them, automatically advancing the opposing team.
Can I send push notifications for upcoming matches?
Yes. Use a scheduled Cloud Function that checks for matches starting in 15 minutes and sends push notifications to team members via Firebase Cloud Messaging. Also notify when scores are confirmed and brackets advance.
Can RapidDev help build a full esports tournament platform?
Yes. RapidDev can implement advanced bracket systems, anti-cheat integrations, streaming embeds, prize pool management, and team communication tools for a complete esports experience.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation