Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Build an Advanced Team Project Tracking System in FlutterFlow

Build a team project management system in FlutterFlow using Firestore subcollections for projects and tasks, a dashboard with progress bars, a Kanban drag-drop board, a Gantt-style timeline, and FCM push notifications when tasks are assigned or status changes. Query only the current user's tasks to stay within Firestore read limits.

What you'll learn

  • Design an efficient Firestore schema using subcollections for projects and tasks
  • Build a dashboard with per-project progress bars bound to Firestore aggregate queries
  • Create a Kanban board with drag-and-drop status columns using Custom Code
  • Send targeted FCM push notifications when tasks are assigned or reach a milestone
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read60-75 minFlutterFlow Free+ (FCM notifications require Firebase Blaze plan)March 2026RapidDev Engineering Team
TL;DR

Build a team project management system in FlutterFlow using Firestore subcollections for projects and tasks, a dashboard with progress bars, a Kanban drag-drop board, a Gantt-style timeline, and FCM push notifications when tasks are assigned or status changes. Query only the current user's tasks to stay within Firestore read limits.

Building a Project Tracker That Scales with Your Team

Most FlutterFlow project management tutorials show you how to create a simple task list. This tutorial goes further: you will build a multi-project, multi-user system with a visual Kanban board, a timeline view for milestones, and real-time notifications. The key architectural decision that separates a working system from one that becomes expensive and slow is Firestore query design. Fetching ALL tasks across ALL projects to display a dashboard is the most common mistake — it burns thousands of reads per page load. This tutorial shows you how to query only what you need, using composites and subcollections to keep reads fast and cheap even as your data grows.

Prerequisites

  • A FlutterFlow project connected to Firebase with Firestore enabled
  • Firebase Cloud Messaging configured for your app (FCM setup in FlutterFlow Push Notifications settings)
  • Understanding of Firestore collections, documents, and queries in FlutterFlow
  • A multi-user auth setup — users must be signed in with Firebase Auth

Step-by-step guide

1

Design the Firestore schema with subcollections

Your data schema is the foundation of everything. Create a top-level projects collection where each document has fields: name (String), description (String), ownerId (String, uid), memberIds (Array of String, uids), status (String: Active / Completed / Archived), dueDate (Timestamp), createdAt (Timestamp), and milestones (Array of Maps with title and dueDate). Create a tasks subcollection inside each project document. Each task document has: title (String), description (String), assigneeId (String), status (String: Todo / In Progress / In Review / Done), priority (String: Low / Medium / High), dueDate (Timestamp), and updatedAt (Timestamp). Using subcollections means you can query all tasks inside one project cheaply using collectionGroup or a direct subcollection reference, without scanning every task across every project.

Expected result: Your Firestore has a projects collection and each project document has a tasks subcollection. The data structure is visible in the Firebase console.

2

Build the dashboard with efficient progress bar queries

Create a DashboardPage. Add a ListView using a Firestore query that fetches only projects where memberIds contains the current user's UID — this ensures each user sees only their projects. For each project card, display a LinearProgressIndicator whose value is completedTaskCount / taskCount (both from the project document — no subcollection reads needed). Show the project name, due date, member avatar row (fetch user avatars from a users collection), and a status chip. Below the project list, add a 'My Tasks' section that queries the top-level or collectionGroup tasks where assigneeId equals the current user's UID and status is not Done, ordered by dueDate, limited to 10 — this section only ever reads 10 documents regardless of how many total tasks exist in the system.

Expected result: The dashboard loads quickly showing only the current user's projects with real-time progress bars. The 'My Tasks' section shows up to 10 upcoming tasks.

3

Create the Kanban board with drag-and-drop columns

Create a KanbanPage for a selected project. Use a horizontal PageView or Row with four columns: Todo, In Progress, In Review, Done. Each column is a DragTarget that accepts task cards. Query the tasks subcollection for the current project and group them by status using a Custom Function that partitions a JSON array into four sub-lists. Render each column as a Column widget with a header chip showing the column name and count, and a ListView of draggable TaskCard Widgets. Each TaskCard is a Draggable widget — when dragging starts, show a semi-transparent drag preview. When dropped on a column DragTarget, call a Custom Action that updates the task's status field in Firestore. Animate the card moving from one column to another using an AnimatedList widget.

updateTaskStatus.dart
1// Custom Action: updateTaskStatus
2import 'package:cloud_firestore/cloud_firestore.dart';
3
4Future<void> updateTaskStatus({
5 required String projectId,
6 required String taskId,
7 required String newStatus,
8}) async {
9 await FirebaseFirestore.instance
10 .collection('projects')
11 .doc(projectId)
12 .collection('tasks')
13 .doc(taskId)
14 .update({
15 'status': newStatus,
16 'updatedAt': FieldValue.serverTimestamp(),
17 });
18
19 // Update denormalized counts on the project document
20 if (newStatus == 'Done') {
21 await FirebaseFirestore.instance
22 .collection('projects')
23 .doc(projectId)
24 .update({'completedTaskCount': FieldValue.increment(1)});
25 }
26}

Expected result: The Kanban board displays all tasks for the selected project grouped into four columns. Dragging a card to a new column immediately moves it and updates the status in Firestore.

4

Build the Gantt-style timeline for milestones

Create a TimelinePage using a Custom Widget wrapping the timeline_tile or timelines Flutter package. Display project milestones as timeline nodes in chronological order. Each node shows the milestone title, target date, and a colored dot: red for overdue, orange for due within 7 days, green for completed. Below the milestone timeline, add a horizontal scrollable bar chart showing each task's start-to-due range per assignee — this is a simplified Gantt view built with a Row of Container widgets whose widths are proportional to the date range. This does not require a specialized Gantt package — just a Custom Widget with date math. Load milestone data from the project document's milestones array field (already fetched when the project was selected).

Expected result: The timeline page displays project milestones in chronological order with color-coded status dots. Overdue milestones are clearly highlighted in red.

5

Send FCM notifications on task assignment and status changes

Create a Cloud Function called notifyOnTaskUpdate triggered by Firestore writes to projects/{projectId}/tasks/{taskId}. Compare the before and after snapshots. If assigneeId changed, send an FCM notification to the new assignee's device token (stored in the users collection under fcmToken). If status changed to 'Done', notify the project owner. Use admin.messaging().send() with a notification payload containing the task title and project name. In FlutterFlow's Push Notifications settings, ensure your app is registered for FCM. On first login, save the device token using FlutterFlow's built-in Update User Action to write the FCM token to the users/{uid} document. The Cloud Function reads this token to send targeted notifications.

functions/index.js
1// functions/index.js (Cloud Function trigger)
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4
5exports.notifyOnTaskUpdate = functions.firestore
6 .document('projects/{projectId}/tasks/{taskId}')
7 .onUpdate(async (change, context) => {
8 const before = change.before.data();
9 const after = change.after.data();
10 const { projectId } = context.params;
11
12 const projectDoc = await admin.firestore().collection('projects').doc(projectId).get();
13 const projectName = projectDoc.data()?.name || 'Your project';
14
15 // Notify new assignee
16 if (before.assigneeId !== after.assigneeId && after.assigneeId) {
17 const userDoc = await admin.firestore().collection('users').doc(after.assigneeId).get();
18 const token = userDoc.data()?.fcmToken;
19 if (token) {
20 await admin.messaging().send({
21 token,
22 notification: {
23 title: 'New task assigned',
24 body: `${after.title} in ${projectName}`,
25 },
26 });
27 }
28 }
29
30 // Notify owner when task is done
31 if (before.status !== 'Done' && after.status === 'Done') {
32 const ownerId = projectDoc.data()?.ownerId;
33 if (ownerId) {
34 const ownerDoc = await admin.firestore().collection('users').doc(ownerId).get();
35 const token = ownerDoc.data()?.fcmToken;
36 if (token) {
37 await admin.messaging().send({
38 token,
39 notification: {
40 title: 'Task completed',
41 body: `${after.title} marked done in ${projectName}`,
42 },
43 });
44 }
45 }
46 }
47 });

Expected result: When a task is assigned to a team member, they receive a push notification within 5 seconds. When a task is marked Done, the project owner receives a completion notification.

Complete working example

projectTrackingQueries.dart
1import 'package:cloud_firestore/cloud_firestore.dart';
2import 'package:firebase_auth/firebase_auth.dart';
3
4final _db = FirebaseFirestore.instance;
5
6// --- Fetch only the current user's projects (efficient) ---
7Stream<QuerySnapshot> getUserProjectsStream() {
8 final uid = FirebaseAuth.instance.currentUser?.uid;
9 if (uid == null) return const Stream.empty();
10
11 return _db
12 .collection('projects')
13 .where('memberIds', arrayContains: uid)
14 .where('status', isEqualTo: 'Active')
15 .orderBy('dueDate')
16 .snapshots();
17}
18
19// --- Fetch up to 10 upcoming tasks for current user (efficient) ---
20Future<List<Map<String, dynamic>>> getMyUpcomingTasks() async {
21 final uid = FirebaseAuth.instance.currentUser?.uid;
22 if (uid == null) return [];
23
24 final snap = await _db
25 .collectionGroup('tasks')
26 .where('assigneeId', isEqualTo: uid)
27 .where('status', whereIn: ['Todo', 'In Progress', 'In Review'])
28 .orderBy('dueDate')
29 .limit(10)
30 .get();
31
32 return snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
33}
34
35// --- Fetch tasks for a Kanban board (one project at a time) ---
36Stream<List<Map<String, dynamic>>> getProjectTasksStream(String projectId) {
37 return _db
38 .collection('projects')
39 .doc(projectId)
40 .collection('tasks')
41 .orderBy('updatedAt', descending: true)
42 .snapshots()
43 .map((snap) =>
44 snap.docs.map((d) => {'id': d.id, ...d.data()}).toList());
45}
46
47// --- Update task status and denormalized count ---
48Future<void> updateTaskStatus({
49 required String projectId,
50 required String taskId,
51 required String newStatus,
52 required String previousStatus,
53}) async {
54 final batch = _db.batch();
55 final taskRef = _db
56 .collection('projects')
57 .doc(projectId)
58 .collection('tasks')
59 .doc(taskId);
60 final projectRef = _db.collection('projects').doc(projectId);
61
62 batch.update(taskRef, {'status': newStatus, 'updatedAt': FieldValue.serverTimestamp()});
63
64 if (newStatus == 'Done' && previousStatus != 'Done') {
65 batch.update(projectRef, {'completedTaskCount': FieldValue.increment(1)});
66 } else if (previousStatus == 'Done' && newStatus != 'Done') {
67 batch.update(projectRef, {'completedTaskCount': FieldValue.increment(-1)});
68 }
69
70 await batch.commit();
71}

Common mistakes when building an Advanced Team Project Tracking System in FlutterFlow

Why it's a problem: Querying ALL tasks across ALL projects on the dashboard page load

How to avoid: Store denormalized taskCount and completedTaskCount counters on each project document. The dashboard reads one project document per project (not its tasks), making progress bars free to display.

Why it's a problem: Not adding Firestore composite indexes for multi-field queries

How to avoid: Run the query in development mode and click the automatic index creation link from the Firestore error message, or manually create composite indexes in the Firebase console before launching.

Why it's a problem: Sending FCM notifications synchronously from the client app instead of Cloud Functions

How to avoid: Always send FCM notifications from a Cloud Function triggered by Firestore writes. The function has the necessary server-side credentials and fires automatically without any extra client-side work.

Best practices

  • Store denormalized task counts on project documents to avoid aggregate queries on the dashboard
  • Use Firestore subcollections for tasks — one subcollection per project prevents cross-project data leaks
  • Limit collectionGroup queries to 10-20 documents with orderBy and limit to keep reads predictable
  • Always set Firestore security rules that verify the requesting user is a member of the project before allowing task reads or writes
  • Send notifications from Cloud Functions triggered by Firestore writes, not from client-side code
  • Create composite indexes before launch — test with realistic data volumes, not just 2-3 test documents
  • Archive rather than delete completed projects so historical data remains available for reporting

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a team project tracking app in FlutterFlow with Firestore. Design an efficient Firestore schema using subcollections for projects and tasks, explain which fields to denormalize on the project document to avoid expensive aggregate reads on the dashboard, and write the Firestore security rules to ensure users can only access projects they are members of.

FlutterFlow Prompt

In my FlutterFlow project, write a Cloud Function for Firebase that triggers on updates to projects/{projectId}/tasks/{taskId}. If assigneeId changes, send an FCM push notification to the new assignee's device token stored in users/{assigneeId}/fcmToken. If status changes to 'Done', notify the project owner.

Frequently asked questions

Can I build a Kanban board in FlutterFlow without Custom Code?

You can approximate a Kanban layout using multiple ListView widgets in a horizontal Row, but drag-and-drop between columns requires Custom Code using Flutter's Draggable and DragTarget widgets. FlutterFlow does not have a built-in drag-and-drop Kanban widget as of March 2026.

How do I prevent one team member from seeing another team's projects?

Set Firestore security rules so that only users whose UID is in the project's memberIds array can read that project and its tasks subcollection. In FlutterFlow, also scope all project queries with a .where('memberIds', arrayContains: currentUserUID) filter — use both the rules and the query filter for defense in depth.

What is the maximum recommended team size for this architecture?

The subcollection architecture works well for teams of up to a few hundred members per project. For larger enterprise use cases (thousands of users per project), consider sharding the tasks subcollection by status or assignee to avoid large collection reads.

How do I build the Gantt timeline view without a specialized package?

Build a horizontal scrollable Row where each assignee has a lane (a Row of Container widgets). Each Container's width represents the number of days between the task start and due date, calculated with date math in a Custom Function. Color the containers by task status. This approach requires no specialized package and works in FlutterFlow with Custom Widgets.

How do FCM notifications know which device to send to?

Each device that installs your app receives a unique FCM registration token. Store this token in the user's Firestore document (users/{uid}/fcmToken) when the user logs in. Cloud Functions then look up the target user's token and pass it to admin.messaging().send() to deliver the notification to their specific device.

Can I use this system for a SaaS product with paying customers?

Yes, but you will need to add workspace isolation — a workspaces collection that owns projects — so that one company's data is completely separated from another's at both the Firestore security rule level and the query level. RapidDev can help architect this multi-tenant layer if you are building a commercial product.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.