Create a gamification system using Firestore badge_definitions with criteria like threshold counts, plus a users/{uid}/earned_badges subcollection for unlocked achievements. Cloud Functions listen for Firestore events (e.g., workout logged, post created) and automatically check if users meet badge criteria. Display badges in a GridView showcase where earned badges appear in full color and unearned ones are greyed out with a CircularPercentIndicator showing progress toward the threshold.
Gamification badges with Cloud Function triggers and progress tracking
Badge systems drive engagement by rewarding users for reaching milestones. This tutorial builds the full system: Firestore badge_definitions with configurable criteria (e.g., complete 10 workouts, post 5 times), Cloud Functions that trigger on relevant Firestore events and check if the user just earned a new badge, a subcollection of earned badges per user, and a visual badge showcase page. Earned badges appear in full color; unearned badges show greyed out with a progress bar. Unlike a points/rewards system (accumulating currency), badges are one-time milestone unlocks.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Cloud Functions enabled (Firebase Blaze plan)
- An existing feature that generates events (e.g., workouts, posts, purchases)
- Basic understanding of Firestore subcollections and Backend Queries
Step-by-step guide
Create the badge_definitions collection with criteria rules
Create the badge_definitions collection with criteria rules
In Firestore, create a badge_definitions collection with fields: name (String — e.g., 'Workout Warrior'), description (String — 'Complete 10 workouts'), iconUrl (String — badge image URL), category (String — fitness, social, learning), criteria (Map with type: 'count', field: 'workouts_completed', threshold: 10), and order (Integer for display sorting). Add 5-8 badge definitions with varied thresholds: First Workout (threshold: 1), Workout Warrior (threshold: 10), Fitness Master (threshold: 50), Social Butterfly (5 posts), Knowledge Seeker (3 courses completed). Store badge images in Firebase Storage or use external icon URLs. Set Firestore rules: public read, admin-only write.
Expected result: Firestore has 5-8 badge definitions with different criteria thresholds and categories.
Set up user progress counters and the earned_badges subcollection
Set up user progress counters and the earned_badges subcollection
On each user document in the users collection, add counter fields that match your badge criteria fields: workouts_completed (Integer, default 0), posts_created (Integer, default 0), courses_completed (Integer, default 0). Create a subcollection users/{uid}/earned_badges with fields: badgeId (String — reference to badge_definitions doc), badgeName (String — denormalized), badgeIconUrl (String — denormalized), and earnedAt (Timestamp). These counters are incremented by Cloud Functions whenever the user completes a relevant action. The earned_badges subcollection records which badges the user has unlocked and when.
Expected result: User documents have progress counter fields and an earned_badges subcollection ready for badge tracking.
Deploy Cloud Functions to check badge criteria on events
Deploy Cloud Functions to check badge criteria on events
Deploy a Cloud Function triggered by Firestore onCreate on the workouts collection. When a new workout is created, the function: (1) reads the workout's userId, (2) increments the user's workouts_completed field by 1 using FieldValue.increment(1), (3) reads all badge_definitions where criteria.field == 'workouts_completed', (4) for each badge, checks if the user's new count >= badge threshold, (5) checks if the badge is already in the user's earned_badges subcollection, and (6) if the user just crossed the threshold and has not earned it yet, creates an earned_badge document and optionally creates a notification. Deploy similar functions for other event types (posts, courses). This server-side approach is secure — users cannot fake badge awards.
1// Cloud Function: onWorkoutCreated2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56exports.onWorkoutCreated = functions.firestore7 .document('workouts/{workoutId}')8 .onCreate(async (snap) => {9 const userId = snap.data().userId;10 const userRef = admin.firestore().collection('users').doc(userId);11 // Increment counter12 await userRef.update({13 workouts_completed: admin.firestore.FieldValue.increment(1)14 });15 // Check badge criteria16 const userDoc = await userRef.get();17 const count = userDoc.data().workouts_completed;18 const badges = await admin.firestore().collection('badge_definitions')19 .where('criteria.field', '==', 'workouts_completed')20 .where('criteria.threshold', '<=', count).get();21 for (const badge of badges.docs) {22 const existing = await userRef.collection('earned_badges')23 .where('badgeId', '==', badge.id).get();24 if (existing.empty) {25 await userRef.collection('earned_badges').add({26 badgeId: badge.id,27 badgeName: badge.data().name,28 badgeIconUrl: badge.data().iconUrl,29 earnedAt: admin.firestore.Timestamp.now(),30 });31 }32 }33 });Expected result: When a user logs a workout, the Cloud Function increments their counter and awards any newly earned badges automatically.
Build the badge showcase GridView with earned and locked states
Build the badge showcase GridView with earned and locked states
Create a BadgesPage with a GridView (crossAxisCount: 3) bound to a Backend Query on badge_definitions ordered by order ascending. Also run a second Backend Query on users/{currentUser.uid}/earned_badges to get the user's earned badge IDs. For each badge in the grid, create a Container card with: an Image widget for the badge icon, a Text for the badge name, and conditional styling — if the badge ID exists in the earned badges list, show the image at full opacity with a gold border and a checkmark overlay; if not earned, apply 0.3 opacity (greyed out) with a lock Icon overlay. Use a Stack widget to layer the badge image, the lock/checkmark overlay, and optionally a CircularPercentIndicator at the bottom showing progress (e.g., currentCount / threshold). Tap an earned badge to show a BottomSheet with the full description and earned date.
Expected result: A grid shows all badges. Earned badges appear colorful with a checkmark. Unearned badges are greyed with a lock and progress indicator.
Display badge progress with CircularPercentIndicator and notifications
Display badge progress with CircularPercentIndicator and notifications
For unearned badges, calculate progress by reading the user's counter field matching the badge's criteria.field and dividing by the threshold. Pass this to a CircularPercentIndicator (from the percent_indicator package) placed below each locked badge card. Set lineWidth to 4, radius to 20, progressColor to primary, and the center Text to the fraction (e.g., '7/10'). For badge award notifications, have the Cloud Function also create a document in users/{uid}/notifications with the badge name and a congratulatory message. On the DashboardPage or AppBar, show a pop-up celebration when a new badge is earned — query earned_badges where earnedAt > lastCheckedTime, and if any exist, show a custom SnackBar or Dialog with the badge image and 'Achievement Unlocked!' text.
Expected result: Unearned badges show a progress ring toward the threshold. When a badge is earned, the user sees a celebratory notification.
Complete working example
1Firestore Data Model:2├── badge_definitions/{badgeId}3│ ├── name: String ("Workout Warrior")4│ ├── description: String ("Complete 10 workouts")5│ ├── iconUrl: String (storage URL or external)6│ ├── category: String ("fitness" | "social" | "learning")7│ ├── criteria: Map8│ │ ├── type: String ("count")9│ │ ├── field: String ("workouts_completed")10│ │ └── threshold: Integer (10)11│ └── order: Integer (display sort)12├── users/{uid}13│ ├── workouts_completed: Integer (7)14│ ├── posts_created: Integer (3)15│ └── courses_completed: Integer (1)16└── users/{uid}/earned_badges/{earnedBadgeId}17 ├── badgeId: String (ref to badge_definitions)18 ├── badgeName: String (denormalized)19 ├── badgeIconUrl: String (denormalized)20 └── earnedAt: Timestamp2122Cloud Function Flow (onWorkoutCreated):231. Read workout.userId242. Increment users/{userId}.workouts_completed253. Query badge_definitions where field==workouts_completed264. For each: if user count >= threshold AND not already earned275. → Create earned_badge doc + send notification2829BadgesPage:30├── AppBar: "My Badges"31├── Text: "Earned: 4 / 8 badges"32└── GridView (crossAxisCount: 3, query: badge_definitions)33 └── Container (per badge)34 ├── Stack35 │ ├── Image (iconUrl, opacity: earned ? 1.0 : 0.3)36 │ ├── Positioned (top-right)37 │ │ ├── Icon(check_circle, green) [if earned]38 │ │ └── Icon(lock, grey) [if not earned]39 │ └── Positioned (bottom-center) [if not earned]40 │ └── CircularPercentIndicator41 │ ├── percent: currentCount / threshold42 │ └── center: Text("7/10")43 ├── Text (badge name, caption, center)44 └── On Tap (if earned) → BottomSheet with description + dateCommon mistakes when building a User Achievement System with Badges in FlutterFlow
Why it's a problem: Checking badge criteria on the client after every action instead of using Cloud Functions
How to avoid: Run all badge criteria checks in Cloud Functions triggered by Firestore write events. This is reliable (runs on every write regardless of client), secure (server-side), and efficient (runs asynchronously without blocking the UI).
Why it's a problem: Not denormalizing badge name and icon on the earned_badges subcollection
How to avoid: When creating an earned_badge document in the Cloud Function, copy badgeName and badgeIconUrl from the badge definition. This allows the badge showcase to display earned badges from a single query.
Why it's a problem: Using a flat array of earned badge IDs on the user document instead of a subcollection
How to avoid: Use a subcollection users/{uid}/earned_badges. Each document stores the badgeId, earnedAt, and denormalized fields. This supports metadata, pagination, and cross-user queries.
Best practices
- Run badge criteria checks in Cloud Functions, never on the client — secure, reliable, and non-blocking
- Denormalize badge name and icon on earned_badge documents to avoid join queries
- Use counter fields on the user document (workouts_completed, posts_created) for efficient threshold comparisons
- Show progress toward unearned badges with CircularPercentIndicator to motivate continued engagement
- Add a celebratory notification or animation when a badge is unlocked to maximize the reward feeling
- Use categories to group badges in the showcase (fitness, social, learning) for better organization
- Test badge awarding with edge cases: exactly at threshold, above threshold, and concurrent events
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for a gamification badge system with badge definitions containing criteria (type, field, threshold) and a per-user earned_badges subcollection. Write a Firebase Cloud Function that triggers on new workout documents, increments the user's counter, checks all relevant badge definitions, and awards any newly earned badges.
Create a badges page with a GridView showing all badge definitions. Earned badges should have full color with a green checkmark overlay. Unearned badges should be greyed out with a lock icon and a small circular progress indicator showing how close the user is to earning them.
Frequently asked questions
How do badges differ from a points or rewards system?
Badges are one-time milestone unlocks (e.g., 'Complete 10 workouts'). Points are accumulating currency earned per action and spent in a rewards store. A user earns a badge once; they earn points repeatedly. Both can coexist in the same app.
Can I add badges retroactively for existing users who already meet the criteria?
Yes. Deploy a one-time Cloud Function that iterates through all users, reads their counter fields, checks against all badge definitions, and creates earned_badge documents for any met criteria. Run this once after deploying the badge system.
How do I show a celebration animation when a badge is earned?
On your main page, run a periodic check (every 30 seconds or on page load) querying earned_badges where earnedAt > lastCheckedTimestamp. If new badges are found, show a custom Dialog or animated overlay with the badge image, confetti, and 'Achievement Unlocked!' text. Update the lastCheckedTimestamp in App State.
Can users display earned badges on their profile page?
Yes. On the profile page, query the user's earned_badges subcollection and display the top 3-5 badges as small circular images in a Row. Tap the row to navigate to the full BadgesPage. This is a common pattern in gaming and social apps.
How do I handle badge criteria based on streaks (consecutive days)?
Add a criteria type of 'streak' with a field like 'current_streak_days' and a threshold. Track the streak in a Cloud Function that runs on daily activity: if the user was active today and yesterday, increment the streak counter; otherwise reset to 1. Check streak badges after updating.
Can RapidDev help build a gamification system with leaderboards and reward tiers?
Yes. A production gamification system with badges, points, leaderboards, tier progression, seasonal challenges, and analytics requires Cloud Functions with complex event processing. RapidDev can architect the complete engagement platform.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation