Build a gaming leaderboard using a Firestore leaderboard_entries collection storing userId, displayName, avatarUrl, score, rank, and lastUpdated. Display ranked entries in a ListView sorted by score descending with gold, silver, and bronze styling for the top three. Use ChoiceChips for time-period filtering (Today, This Week, All Time) and validate all score submissions through a Cloud Function to prevent cheating.
Real-time ranked leaderboard with score validation and time filters
A leaderboard drives competition and engagement in gaming and community apps. This tutorial builds one in FlutterFlow: a Firestore collection stores player scores with ranking metadata, the UI shows a highlighted top-3 podium above a scrollable ranked list, ChoiceChips filter by time period, and a Cloud Function validates score submissions to prevent client-side manipulation. The leaderboard updates in real time so players see rank changes instantly.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Authentication enabled with user profiles
- Basic understanding of Backend Queries and Conditional Styling
- Cloud Functions enabled for server-side score validation
Step-by-step guide
Create the Firestore leaderboard data model
Create the Firestore leaderboard data model
Create a leaderboard_entries collection with fields: userId (String), displayName (String), avatarUrl (String), score (Integer), rank (Integer), lastUpdated (Timestamp), and gameId (String, for apps with multiple games or levels). Set Firestore rules: any authenticated user can read leaderboard entries, but only Cloud Functions (admin SDK) can write scores. This prevents users from directly manipulating their scores via the Firestore API. Create a composite index on gameId + score (descending) for efficient sorted queries. For time-based filtering, the lastUpdated timestamp is used to filter entries within date ranges.
Expected result: A leaderboard_entries collection is ready with proper fields, indexes, and security rules allowing reads but restricting writes to Cloud Functions.
Build the top-3 podium display
Build the top-3 podium display
Create a LeaderboardPage. At the top, add a Container for the podium section. Query leaderboard_entries ordered by score descending, limit 3, filtered by the selected time period. Display the top 3 in a Row with specific styling: the center position (1st place) is elevated higher using padding or a taller Container, 2nd place is on the left, 3rd on the right. Each podium position shows: CircleImage (avatar) with a colored border (gold #FFD700 for 1st, silver #C0C0C0 for 2nd, bronze #CD7F32 for 3rd), Text (displayName), Text (score, bold), and a rank badge Container with the number. Use Conditional Styling to apply the correct medal color based on the index. Add a crown or trophy Icon above the 1st place avatar for extra visual emphasis.
Expected result: The top 3 players appear in a podium layout with gold, silver, and bronze colored badges and their scores prominently displayed.
Display the full ranked list below the podium
Display the full ranked list below the podium
Below the podium, add a ListView bound to a Backend Query on leaderboard_entries ordered by score descending, limit 50, with infinite scroll ON. Set Single Time Query to OFF for real-time updates. Each item is a Row containing: a rank number Text (use the item index + 1 since the query is sorted), CircleImage (avatar, small), Column with displayName and score, and a trailing Text showing the score. Highlight the current user's row: use Conditional Styling to add a distinct background color (light blue or your theme accent) when the entry's userId matches the current user UID. Add a 'You' badge Text next to the current user's name. For the top 3 items in this list, you can either hide them (since they are in the podium) or show them with their medal colors for consistency.
Expected result: A scrollable ranked list shows all players with their rank, avatar, name, and score. The current user's row is highlighted.
Add time-period filtering with ChoiceChips
Add time-period filtering with ChoiceChips
Above the podium, add a Row of ChoiceChips with options: Today, This Week, All Time. Bind the selected value to a Page State variable timePeriod (String). Modify both the podium and list Backend Queries to filter by lastUpdated based on the selected period: for Today, filter where lastUpdated >= start of today; for This Week, filter where lastUpdated >= start of current week (Monday); for All Time, no date filter. Create a Custom Function that returns the start timestamp for each period. When the user taps a different chip, the queries re-execute with the new filter and both the podium and list update. Default to 'All Time' on page load for the broadest view.
Expected result: Switching between Today, This Week, and All Time filters the leaderboard to show only scores from that period.
Validate and submit scores through a Cloud Function
Validate and submit scores through a Cloud Function
Create a callable Cloud Function named submitScore that receives the score and gameId. The function validates: (1) the caller is authenticated; (2) the score is a positive integer within a reasonable range for the game (e.g., 0-1,000,000); (3) optionally, verify game-specific rules (time played, level completed, etc.). If valid, use a Firestore transaction to read the user's current leaderboard entry: if no entry exists, create one; if the new score is higher than the current score, update it; otherwise, ignore (keep the high score). Set lastUpdated to the server timestamp and recalculate ranks by querying all entries sorted by score and updating rank fields. In FlutterFlow, call this Cloud Function via a Backend Call action at the end of a game or achievement, passing the score.
1// Cloud Function: submitScore2const functions = require('firebase-functions');3const admin = require('firebase-admin');45exports.submitScore = functions.https.onCall(async (data, context) => {6 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');7 const { score, gameId } = data;8 if (!Number.isInteger(score) || score < 0 || score > 1000000) {9 throw new functions.https.HttpsError('invalid-argument', 'Invalid score');10 }1112 const uid = context.auth.uid;13 const userDoc = await admin.firestore().collection('users').doc(uid).get();14 const entryRef = admin.firestore().collection('leaderboard_entries').doc(`${gameId}_${uid}`);1516 await admin.firestore().runTransaction(async (txn) => {17 const entry = await txn.get(entryRef);18 if (!entry.exists || score > entry.data().score) {19 txn.set(entryRef, {20 userId: uid,21 displayName: userDoc.data().displayName || 'Anonymous',22 avatarUrl: userDoc.data().avatarUrl || '',23 score: score,24 gameId: gameId,25 lastUpdated: admin.firestore.FieldValue.serverTimestamp(),26 }, { merge: true });27 }28 });29 return { success: true };30});Expected result: Scores are submitted through the Cloud Function, validated server-side, and only the highest score per user is stored.
Complete working example
1Firestore Data Model:2└── leaderboard_entries/{gameId}_{userId}3 ├── userId: String4 ├── displayName: String ("PlayerOne")5 ├── avatarUrl: String6 ├── score: Integer (9850)7 ├── gameId: String ("puzzle_mode")8 └── lastUpdated: Timestamp910Firestore Rules:11match /leaderboard_entries/{entryId} {12 allow read: if request.auth != null;13 allow write: if false; // Only Cloud Functions can write14}1516Leaderboard Page:17├── ChoiceChips (Today | This Week | All Time)18│ └── On Select → Update Page State timePeriod → re-query19├── Podium Section (top 3)20│ └── Row21│ ├── 2nd Place (left, silver #C0C0C0)22│ │ ├── CircleImage (avatar, silver border)23│ │ ├── Text (displayName)24│ │ └── Text (score)25│ ├── 1st Place (center, elevated, gold #FFD700)26│ │ ├── Icon (crown/trophy)27│ │ ├── CircleImage (avatar, gold border)28│ │ ├── Text (displayName)29│ │ └── Text (score, large)30│ └── 3rd Place (right, bronze #CD7F32)31│ ├── CircleImage (avatar, bronze border)32│ ├── Text (displayName)33│ └── Text (score)34└── ListView (full ranked list, score DESC, limit 50, real-time)35 └── Row36 ├── Text (rank #)37 ├── CircleImage (avatar, small)38 ├── Column → Text (displayName) + Badge ("You", conditional)39 ├── Text (score)40 └── Conditional Styling: highlight if userId == currentUser4142Score Submission Flow:43├── Game/activity completes → score calculated44├── Backend Call → submitScore Cloud Function (score, gameId)45├── Cloud Function validates → transaction updates if new high score46└── Leaderboard real-time query picks up the changeCommon mistakes when creating a Gaming Leaderboard or Community Platform Using FlutterFlow
Why it's a problem: Letting clients write scores directly to Firestore without Cloud Function validation
How to avoid: Set Firestore rules to block all direct writes to leaderboard_entries. Route all score submissions through a Cloud Function that validates the score against game rules before writing.
Why it's a problem: Creating a new leaderboard entry every time a score is submitted instead of updating the existing one
How to avoid: Use the userId (or gameId_userId) as the document ID. Use set with merge to upsert. In the Cloud Function, only update if the new score exceeds the current high score.
Why it's a problem: Not adding composite Firestore indexes for sorted and filtered queries
How to avoid: Create composite indexes in the Firebase Console for: gameId (ascending) + score (descending), and gameId (ascending) + lastUpdated (ascending) + score (descending). Firestore error logs include a direct link to create the missing index.
Best practices
- Use Cloud Functions for all score writes to prevent client-side score manipulation
- Use userId or gameId_userId as document ID to ensure one entry per player per game
- Set Single Time Query to OFF for real-time leaderboard updates
- Add composite Firestore indexes for filtered and sorted leaderboard queries
- Highlight the current user's rank in the list for immediate personal context
- Limit the displayed list to 50-100 entries with infinite scroll to control read costs
- Store the user's displayName and avatar on the leaderboard entry to avoid N+1 user doc lookups
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Firebase Cloud Function that validates and submits a game score to a Firestore leaderboard. It should use a transaction to only update if the new score is higher than the existing one. Include input validation for score range.
Create a leaderboard page with a podium section showing the top 3 players with gold, silver, and bronze styling, followed by a scrollable ranked list. Add ChoiceChips to filter by Today, This Week, and All Time.
Frequently asked questions
How do I show the current user's rank even if they are not in the top 50?
Run a separate Backend Query that fetches the current user's leaderboard entry by document ID (gameId_userId). Display their rank, score, and position in a fixed bar at the bottom of the page, outside the scrollable list. This gives personal context regardless of where they rank.
How do I recalculate ranks after a new score is submitted?
For small leaderboards (under 1,000 entries), recalculate ranks in the Cloud Function after each score update by querying all entries sorted by score descending and updating the rank field sequentially. For large leaderboards, skip the rank field and derive rank from the list index in the UI.
Can I add a friends-only leaderboard?
Yes. Query the user's friends list (from a following or friends subcollection), then query leaderboard_entries where userId is in the friends list using whereIn (batched for lists over 10). Display this as a separate tab alongside the global leaderboard.
How do I prevent the same user from appearing multiple times?
Use the userId (or gameId_userId combo) as the Firestore document ID. Since document IDs are unique, the same user can only have one entry. Use set with merge to update existing entries rather than creating new ones.
Should I update the leaderboard in real time or on refresh?
Use real-time queries (Single Time Query OFF) for competitive leaderboards where players want to see rank changes instantly. For casual leaderboards, a pull-to-refresh approach is cheaper on Firestore reads. The choice depends on how competitive your game is.
Can RapidDev help build a competitive gaming platform?
Yes. A production gaming platform needs anti-cheat detection, ELO rating systems, matchmaking, tournament brackets, reward distribution, and real-time event broadcasting. RapidDev can architect the full competitive infrastructure beyond basic leaderboards.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation