Live sports scores in FlutterFlow work by running a single Cloud Function on a 60-second schedule that polls a sports API and writes updated scores to Firestore. The FlutterFlow app subscribes to those Firestore documents via real-time Backend Queries. For score change alerts, the same Cloud Function triggers FCM push notifications to users who favorited the relevant teams. Never poll the sports API from each client device.
Live Sports Data via Server-Side Polling and Firestore Fan-out
Sports apps need data that changes every few minutes during live games — goals, fouls, quarter scores, and game state. The architecture that scales is simple: one Cloud Function polls the sports API (The Sports DB, API-SPORTS, or a similar provider) on a schedule, writes the latest data to Firestore, and lets Firestore distribute updates to all connected clients via its real-time listeners. This means a single API call serves thousands of users, and each client pays zero additional API cost regardless of how many viewers watch the same game.
Prerequisites
- A Firebase project with Firestore, Cloud Functions, and Cloud Messaging enabled (Blaze plan)
- A FlutterFlow project connected to that Firebase project
- An API key from a sports data provider (The Sports DB free tier works for demos, API-SPORTS for production)
- Basic familiarity with FlutterFlow Backend Queries and Action Flows
Step-by-step guide
Design the Firestore schema for games, teams, and standings
Design the Firestore schema for games, teams, and standings
Create four Firestore collections. The 'games' collection stores each match with fields: id, home_team_id, away_team_id, home_score (Integer), away_score (Integer), status (String: scheduled/live/final), start_time (Timestamp), league_id, sport, and last_updated (Timestamp). The 'teams' collection stores team metadata: id, name, logo_url, sport, league_id, wins, losses, draws. The 'standings' collection stores current league standings as a ranked array per league document. The 'user_favorites' collection stores per-user team preferences: one document per user (keyed by UID) with a team_ids Array field. In FlutterFlow, import all four collections via the Firestore panel.
Expected result: All four collections appear in FlutterFlow's Firestore panel. Manually create one test game document to verify field types are mapped correctly.
Deploy the scheduled polling Cloud Function
Deploy the scheduled polling Cloud Function
Create a Cloud Function called 'pollSportsScores' using the Firebase Cloud Functions PubSub scheduler, set to run every 1 minute. The function calls your sports API for live games (any game with status 'live' or scheduled to start within 15 minutes), parses the response, and does a batch write to Firestore updating all changed game documents. Include error handling with exponential backoff for API rate limit responses (HTTP 429). Log the number of games updated each run so you can monitor in Cloud Functions logs. Deploy using firebase deploy --only functions.
1// functions/pollSportsScores.js2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const axios = require('axios');56if (!admin.apps.length) admin.initializeApp();7const db = admin.firestore();89exports.pollSportsScores = functions.pubsub10 .schedule('every 1 minutes')11 .onRun(async () => {12 const apiKey = functions.config().sports.api_key;13 const baseUrl = 'https://v3.football.api-sports.io';14 const today = new Date().toISOString().split('T')[0];1516 let response;17 try {18 response = await axios.get(`${baseUrl}/fixtures?date=${today}&status=1H-2H-HT-ET-BT`, {19 headers: { 'x-apisports-key': apiKey },20 timeout: 800021 });22 } catch (err) {23 console.error('Sports API error:', err.message);24 return null;25 }2627 const fixtures = response.data?.response || [];28 if (!fixtures.length) { console.log('No live fixtures'); return null; }2930 // Read current Firestore scores to detect changes31 const batch = db.batch();32 const changeNotifications = [];3334 for (const fixture of fixtures) {35 const gameRef = db.collection('games').doc(String(fixture.fixture.id));36 const existingSnap = await gameRef.get();37 const existing = existingSnap.data();3839 const homeScore = fixture.goals.home || 0;40 const awayScore = fixture.goals.away || 0;4142 // Detect score change for push notification43 if (existing && (existing.home_score !== homeScore || existing.away_score !== awayScore)) {44 changeNotifications.push({45 home_team_id: String(fixture.teams.home.id),46 away_team_id: String(fixture.teams.away.id),47 home_score: homeScore,48 away_score: awayScore,49 home_name: fixture.teams.home.name,50 away_name: fixture.teams.away.name,51 });52 }5354 batch.set(gameRef, {55 id: String(fixture.fixture.id),56 home_team_id: String(fixture.teams.home.id),57 away_team_id: String(fixture.teams.away.id),58 home_team_name: fixture.teams.home.name,59 away_team_name: fixture.teams.away.name,60 home_team_logo: fixture.teams.home.logo,61 away_team_logo: fixture.teams.away.logo,62 home_score: homeScore,63 away_score: awayScore,64 status: fixture.fixture.status.short,65 elapsed: fixture.fixture.status.elapsed,66 last_updated: admin.firestore.FieldValue.serverTimestamp()67 }, { merge: true });68 }6970 await batch.commit();71 console.log(`Updated ${fixtures.length} live games, ${changeNotifications.length} score changes`);7273 // Send push notifications for score changes74 for (const change of changeNotifications) {75 await sendScoreNotification(change);76 }77 return null;78 });7980async function sendScoreNotification(change) {81 // Find users who favorited either team82 const favSnap = await db.collection('user_favorites')83 .where('team_ids', 'array-contains-any',84 [change.home_team_id, change.away_team_id])85 .get();86 if (favSnap.empty) return;8788 const tokens = [];89 favSnap.docs.forEach(doc => {90 const token = doc.data().fcm_token;91 if (token) tokens.push(token);92 });93 if (!tokens.length) return;9495 await admin.messaging().sendMulticast({96 tokens,97 notification: {98 title: `${change.home_name} ${change.home_score} - ${change.away_score} ${change.away_name}`,99 body: 'Score update'100 },101 data: { type: 'score_update' }102 });103}Expected result: The Cloud Function runs every minute. Check Cloud Functions logs in Firebase console — you should see 'Updated X live games' entries. Check Firestore to confirm game documents are being created and updated.
Build the live scores screen with real-time Backend Queries
Build the live scores screen with real-time Backend Queries
In FlutterFlow, create a 'LiveScoresPage'. Add a Backend Query at the page level that queries the 'games' collection filtered by status equal to 'live', ordered by start_time, with real-time listening enabled. Below a 'LIVE' header with a pulsing red dot animation (created with an Animated Container cycling between full and half opacity), add a ListView bound to this query. Each list item is a 'ScoreCard' Component showing both team logos side-by-side, both team names, the current score in a large bold font, the game elapsed time (e.g., '67'''), and a status badge. Add a second ListView for 'Today's Games' filtered by today's date and status not equal to 'live', to show upcoming and completed fixtures.
Expected result: The live scores page shows real-time updating scores. When you manually change a score value in Firestore, the UI updates within 1-2 seconds without any page refresh.
Add a favorite teams feature with FCM notifications
Add a favorite teams feature with FCM notifications
Create a 'TeamsPage' that lists all teams from the 'teams' Firestore collection. Each team row has a star/heart icon button. On tap, the action flow checks if the team ID is already in the user's user_favorites document: if yes, remove it; if no, add it. Also store the user's FCM device token in the user_favorites document (fetch it via a Custom Action using FirebaseMessaging.instance.getToken()). When the pollSportsScores Cloud Function detects a score change, it queries user_favorites where team_ids array-contains the team ID and sends an FCM push notification to those users. In FlutterFlow, add a Custom Action in your app's initState to request notification permissions and save the FCM token.
1// custom_actions/save_fcm_token.dart2import 'package:firebase_messaging/firebase_messaging.dart';3import 'package:cloud_firestore/cloud_firestore.dart';4import 'package:firebase_auth/firebase_auth.dart';56Future<void> saveFcmToken() async {7 final messaging = FirebaseMessaging.instance;8 final settings = await messaging.requestPermission();9 if (settings.authorizationStatus != AuthorizationStatus.authorized) return;10 final token = await messaging.getToken();11 if (token == null) return;12 final uid = FirebaseAuth.instance.currentUser?.uid;13 if (uid == null) return;14 await FirebaseFirestore.instance15 .collection('user_favorites')16 .doc(uid)17 .set({'fcm_token': token}, SetOptions(merge: true));18}Expected result: After enabling notifications, the user's FCM token appears in their user_favorites document. When a manually-triggered score change occurs in Firestore, a push notification arrives on the user's device within 30-60 seconds.
Build a standings and schedule tab UI
Build a standings and schedule tab UI
Add a TabBar at the top of your sports page with three tabs: 'Live', 'Schedule', and 'Standings'. The Schedule tab shows a ListView of all today's games plus the next 7 days, filtered by status 'scheduled'. Add a DatePicker row at the top of the schedule so users can browse past results. The Standings tab reads from the 'standings' Firestore collection, which is updated by a second scheduled Cloud Function that runs once per day. Display standings as a table with columns: Position, Team (with logo), Played, Won, Drawn, Lost, Points. Add a league selector at the top (a horizontal ScrollView of league logo chips) to switch between competitions.
Expected result: All three tabs work independently. Live tab shows real-time scores, Schedule tab shows upcoming fixtures, and Standings tab shows the current league table.
Complete working example
1// sportsUpdate.js — Complete sports data pipeline2// Polls API every minute for live scores3// Updates standings once per day4// Sends FCM push notifications on score changes56const functions = require('firebase-functions');7const admin = require('firebase-admin');8const axios = require('axios');910if (!admin.apps.length) admin.initializeApp();11const db = admin.firestore();1213const API_KEY = () => functions.config().sports?.api_key;14const BASE_URL = 'https://v3.football.api-sports.io';15const HEADERS = () => ({ 'x-apisports-key': API_KEY(), 'Content-Type': 'application/json' });1617async function fetchWithRetry(url, retries = 2) {18 for (let i = 0; i <= retries; i++) {19 try {20 return await axios.get(url, { headers: HEADERS(), timeout: 8000 });21 } catch (e) {22 if (e.response?.status === 429 && i < retries) {23 await new Promise(r => setTimeout(r, 2000 * (i + 1)));24 } else throw e;25 }26 }27}2829exports.pollLiveScores = functions.pubsub30 .schedule('every 1 minutes').onRun(async () => {31 const today = new Date().toISOString().split('T')[0];32 let res;33 try {34 res = await fetchWithRetry(`${BASE_URL}/fixtures?date=${today}&live=all`);35 } catch (e) {36 console.error('API fetch failed:', e.message);37 return null;38 }39 const fixtures = res.data?.response || [];40 if (!fixtures.length) return null;4142 const scoreChanges = [];43 const batch = db.batch();4445 for (const f of fixtures) {46 const gameId = String(f.fixture.id);47 const ref = db.collection('games').doc(gameId);48 const snap = await ref.get();49 const prev = snap.data();50 const homeScore = f.goals.home ?? 0;51 const awayScore = f.goals.away ?? 0;5253 if (prev && (prev.home_score !== homeScore || prev.away_score !== awayScore)) {54 scoreChanges.push({55 home_team_id: String(f.teams.home.id),56 away_team_id: String(f.teams.away.id),57 home_name: f.teams.home.name,58 away_name: f.teams.away.name,59 home_score: homeScore,60 away_score: awayScore,61 });62 }6364 batch.set(ref, {65 home_team_name: f.teams.home.name,66 away_team_name: f.teams.away.name,67 home_team_logo: f.teams.home.logo,68 away_team_logo: f.teams.away.logo,69 home_team_id: String(f.teams.home.id),70 away_team_id: String(f.teams.away.id),71 home_score: homeScore,72 away_score: awayScore,73 status: f.fixture.status.short,74 elapsed: f.fixture.status.elapsed,75 league_name: f.league.name,76 league_logo: f.league.logo,77 last_updated: admin.firestore.FieldValue.serverTimestamp(),78 }, { merge: true });79 }8081 await batch.commit();82 await Promise.all(scoreChanges.map(notifyFavoriteUsers));83 console.log(`Synced ${fixtures.length} games, ${scoreChanges.length} score changes`);84 return null;85 });8687async function notifyFavoriteUsers(change) {88 const snap = await db.collection('user_favorites')89 .where('team_ids', 'array-contains-any',90 [change.home_team_id, change.away_team_id]).limit(500).get();91 if (snap.empty) return;92 const tokens = snap.docs.map(d => d.data().fcm_token).filter(Boolean);93 if (!tokens.length) return;94 const chunks = [];95 for (let i = 0; i < tokens.length; i += 500) chunks.push(tokens.slice(i, i + 500));96 await Promise.all(chunks.map(chunk =>97 admin.messaging().sendMulticast({98 tokens: chunk,99 notification: {100 title: `${change.home_name} ${change.home_score}-${change.away_score} ${change.away_name}`,101 body: 'Score update from your favorite match'102 },103 data: { type: 'score_update', home_team_id: change.home_team_id }104 })105 ));106}Common mistakes when creating a Sports Scores and Live Updates Feature in FlutterFlow
Why it's a problem: Polling the sports API from every client device individually
How to avoid: Use a single scheduled Cloud Function as the sole API caller. All clients read from Firestore, which is designed to serve the same data to unlimited concurrent readers cheaply.
Why it's a problem: Sending a push notification for every Firestore write regardless of change
How to avoid: Compare the new score with the previous Firestore value before sending. Only trigger FCM when home_score or away_score has actually changed.
Why it's a problem: Forgetting to request notification permissions before saving the FCM token
How to avoid: Always await requestPermission() first, check that authorizationStatus is 'authorized', then call getToken(). Implement this in the app's first-launch flow with a clear explanation of why notifications are useful.
Best practices
- Use a single scheduled Cloud Function as the sole sports API caller — fan out to all clients via Firestore real-time listeners.
- Store your sports API key in Firebase Functions config (firebase functions:config:set sports.api_key=YOUR_KEY) and never in client-side code.
- Mark game documents as 'final' when the game ends and stop updating them to reduce unnecessary Firestore writes.
- Add a 'last_api_call' timestamp document to track polling health — if it's more than 3 minutes old, your scheduled function may have stalled.
- Implement FCM token refresh handling: listen to FirebaseMessaging.onTokenRefresh and update the stored token in Firestore.
- Cap FCM notifications to a maximum of 3 per game per user to avoid notification fatigue during high-scoring matches.
- Cache team logos in Flutter's image cache — they don't change often but are loaded frequently. Use CachedNetworkImage package after code export for automatic disk caching.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a Firebase Cloud Function that polls the API-Sports football API every minute to get live match scores. Write a Node.js function that: fetches all live fixtures for today, compares the new scores with values stored in a Firestore 'games' collection, performs a batch update to Firestore with the latest scores, and returns a list of score change events. Include error handling for API rate limit responses (HTTP 429) with exponential backoff retry.
In my FlutterFlow project I have a 'games' Firestore collection with fields: home_team_name, away_team_name, home_score, away_score, status, and elapsed. I want to display live games in a ListView that updates in real-time as scores change. Walk me through setting up a Backend Query on the games collection filtered to only show documents where status is 'live', with real-time listening enabled, and binding the score fields to Text widgets in a ScoreCard component.
Frequently asked questions
Which sports data API should I use?
The Sports DB is free for non-commercial use with historical data. API-Sports (api-sports.io) offers live data from $14/month and covers football, basketball, baseball, hockey, and more. Sportradar and Stats Perform are enterprise-tier options for production apps with SLA guarantees.
How do I handle time zones for game schedules?
Store all timestamps in Firestore as UTC. When displaying to users, use FlutterFlow's DateTime formatting with the device's local timezone, or include a timezone selector for users in multiple regions.
Can I show in-game events like goals and yellow cards, not just scores?
Yes. Most sports APIs return event arrays per fixture (goal at minute 23, yellow card at minute 45). Extend your game document schema with an 'events' Array field and update it in the polling function. Display events as a scrollable timeline below the score in the game detail view.
What happens when the Cloud Function fails and scores stop updating?
Add a health check document in Firestore (system/polling_health) that the function updates with a timestamp on each run. Display a 'scores may be delayed' banner in the app if last_updated is more than 3 minutes ago.
How do I add live commentary or play-by-play text?
Use a sports API endpoint that provides commentary events (most premium APIs include this). Store commentary as a sub-collection under each game document and display it as a real-time updating ListView — new entries appear at the top as the function writes them.
Can I monetize the sports feature with ads around live scores?
Yes. Google AdMob integrates with FlutterFlow via Custom Widgets after code export. Place banner ads below the live scores list and interstitial ads between navigating from the scores list to a game detail view. Follow sports data provider terms of service — many prohibit commercial use on free API tiers.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation