# How to Create a Sports Scores and Live Updates Feature in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 45-60 min
- Compatibility: FlutterFlow Free+ (Cloud Functions and FCM require Firebase Blaze plan)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

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

```
// functions/pollSportsScores.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

if (!admin.apps.length) admin.initializeApp();
const db = admin.firestore();

exports.pollSportsScores = functions.pubsub
  .schedule('every 1 minutes')
  .onRun(async () => {
    const apiKey = functions.config().sports.api_key;
    const baseUrl = 'https://v3.football.api-sports.io';
    const today = new Date().toISOString().split('T')[0];

    let response;
    try {
      response = await axios.get(`${baseUrl}/fixtures?date=${today}&status=1H-2H-HT-ET-BT`, {
        headers: { 'x-apisports-key': apiKey },
        timeout: 8000
      });
    } catch (err) {
      console.error('Sports API error:', err.message);
      return null;
    }

    const fixtures = response.data?.response || [];
    if (!fixtures.length) { console.log('No live fixtures'); return null; }

    // Read current Firestore scores to detect changes
    const batch = db.batch();
    const changeNotifications = [];

    for (const fixture of fixtures) {
      const gameRef = db.collection('games').doc(String(fixture.fixture.id));
      const existingSnap = await gameRef.get();
      const existing = existingSnap.data();

      const homeScore = fixture.goals.home || 0;
      const awayScore = fixture.goals.away || 0;

      // Detect score change for push notification
      if (existing && (existing.home_score !== homeScore || existing.away_score !== awayScore)) {
        changeNotifications.push({
          home_team_id: String(fixture.teams.home.id),
          away_team_id: String(fixture.teams.away.id),
          home_score: homeScore,
          away_score: awayScore,
          home_name: fixture.teams.home.name,
          away_name: fixture.teams.away.name,
        });
      }

      batch.set(gameRef, {
        id: String(fixture.fixture.id),
        home_team_id: String(fixture.teams.home.id),
        away_team_id: String(fixture.teams.away.id),
        home_team_name: fixture.teams.home.name,
        away_team_name: fixture.teams.away.name,
        home_team_logo: fixture.teams.home.logo,
        away_team_logo: fixture.teams.away.logo,
        home_score: homeScore,
        away_score: awayScore,
        status: fixture.fixture.status.short,
        elapsed: fixture.fixture.status.elapsed,
        last_updated: admin.firestore.FieldValue.serverTimestamp()
      }, { merge: true });
    }

    await batch.commit();
    console.log(`Updated ${fixtures.length} live games, ${changeNotifications.length} score changes`);

    // Send push notifications for score changes
    for (const change of changeNotifications) {
      await sendScoreNotification(change);
    }
    return null;
  });

async function sendScoreNotification(change) {
  // Find users who favorited either team
  const favSnap = await db.collection('user_favorites')
    .where('team_ids', 'array-contains-any',
      [change.home_team_id, change.away_team_id])
    .get();
  if (favSnap.empty) return;

  const tokens = [];
  favSnap.docs.forEach(doc => {
    const token = doc.data().fcm_token;
    if (token) tokens.push(token);
  });
  if (!tokens.length) return;

  await admin.messaging().sendMulticast({
    tokens,
    notification: {
      title: `${change.home_name} ${change.home_score} - ${change.away_score} ${change.away_name}`,
      body: 'Score update'
    },
    data: { type: 'score_update' }
  });
}
```

**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.

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

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

```
// custom_actions/save_fcm_token.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

Future<void> saveFcmToken() async {
  final messaging = FirebaseMessaging.instance;
  final settings = await messaging.requestPermission();
  if (settings.authorizationStatus != AuthorizationStatus.authorized) return;
  final token = await messaging.getToken();
  if (token == null) return;
  final uid = FirebaseAuth.instance.currentUser?.uid;
  if (uid == null) return;
  await FirebaseFirestore.instance
      .collection('user_favorites')
      .doc(uid)
      .set({'fcm_token': token}, SetOptions(merge: true));
}
```

**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.

### 5. 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 code example

File: `functions/sportsUpdate.js`

```javascript
// sportsUpdate.js — Complete sports data pipeline
// Polls API every minute for live scores
// Updates standings once per day
// Sends FCM push notifications on score changes

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

if (!admin.apps.length) admin.initializeApp();
const db = admin.firestore();

const API_KEY = () => functions.config().sports?.api_key;
const BASE_URL = 'https://v3.football.api-sports.io';
const HEADERS = () => ({ 'x-apisports-key': API_KEY(), 'Content-Type': 'application/json' });

async function fetchWithRetry(url, retries = 2) {
  for (let i = 0; i <= retries; i++) {
    try {
      return await axios.get(url, { headers: HEADERS(), timeout: 8000 });
    } catch (e) {
      if (e.response?.status === 429 && i < retries) {
        await new Promise(r => setTimeout(r, 2000 * (i + 1)));
      } else throw e;
    }
  }
}

exports.pollLiveScores = functions.pubsub
  .schedule('every 1 minutes').onRun(async () => {
    const today = new Date().toISOString().split('T')[0];
    let res;
    try {
      res = await fetchWithRetry(`${BASE_URL}/fixtures?date=${today}&live=all`);
    } catch (e) {
      console.error('API fetch failed:', e.message);
      return null;
    }
    const fixtures = res.data?.response || [];
    if (!fixtures.length) return null;

    const scoreChanges = [];
    const batch = db.batch();

    for (const f of fixtures) {
      const gameId = String(f.fixture.id);
      const ref = db.collection('games').doc(gameId);
      const snap = await ref.get();
      const prev = snap.data();
      const homeScore = f.goals.home ?? 0;
      const awayScore = f.goals.away ?? 0;

      if (prev && (prev.home_score !== homeScore || prev.away_score !== awayScore)) {
        scoreChanges.push({
          home_team_id: String(f.teams.home.id),
          away_team_id: String(f.teams.away.id),
          home_name: f.teams.home.name,
          away_name: f.teams.away.name,
          home_score: homeScore,
          away_score: awayScore,
        });
      }

      batch.set(ref, {
        home_team_name: f.teams.home.name,
        away_team_name: f.teams.away.name,
        home_team_logo: f.teams.home.logo,
        away_team_logo: f.teams.away.logo,
        home_team_id: String(f.teams.home.id),
        away_team_id: String(f.teams.away.id),
        home_score: homeScore,
        away_score: awayScore,
        status: f.fixture.status.short,
        elapsed: f.fixture.status.elapsed,
        league_name: f.league.name,
        league_logo: f.league.logo,
        last_updated: admin.firestore.FieldValue.serverTimestamp(),
      }, { merge: true });
    }

    await batch.commit();
    await Promise.all(scoreChanges.map(notifyFavoriteUsers));
    console.log(`Synced ${fixtures.length} games, ${scoreChanges.length} score changes`);
    return null;
  });

async function notifyFavoriteUsers(change) {
  const snap = await db.collection('user_favorites')
    .where('team_ids', 'array-contains-any',
      [change.home_team_id, change.away_team_id]).limit(500).get();
  if (snap.empty) return;
  const tokens = snap.docs.map(d => d.data().fcm_token).filter(Boolean);
  if (!tokens.length) return;
  const chunks = [];
  for (let i = 0; i < tokens.length; i += 500) chunks.push(tokens.slice(i, i + 500));
  await Promise.all(chunks.map(chunk =>
    admin.messaging().sendMulticast({
      tokens: chunk,
      notification: {
        title: `${change.home_name} ${change.home_score}-${change.away_score} ${change.away_name}`,
        body: 'Score update from your favorite match'
      },
      data: { type: 'score_update', home_team_id: change.home_team_id }
    })
  ));
}
```

## Common mistakes

- **Polling the sports API from every client device individually** — Most sports APIs charge per request and enforce rate limits (often 100 requests per minute). With 1,000 concurrent users each polling every 60 seconds, you hit 1,000 requests per minute — immediately exceeding limits and generating enormous API costs. Fix: 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.
- **Sending a push notification for every Firestore write regardless of change** — If you trigger notifications on every game document update (not just score changes), users receive a notification every 60 seconds during a game — dozens of notifications per match that quickly cause users to disable them. Fix: Compare the new score with the previous Firestore value before sending. Only trigger FCM when home_score or away_score has actually changed.
- **Forgetting to request notification permissions before saving the FCM token** — On iOS, you must explicitly call FirebaseMessaging.requestPermission() and wait for user approval before getToken() returns a valid token. Calling getToken() without permission returns null and the user never receives notifications. Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-sports-scores-and-updates-feature-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-sports-scores-and-updates-feature-in-flutterflow
