Multi-user collaboration uses Firestore's real-time listeners to keep all users' views in sync. Store tasks as a subcollection (not an array) on the board document so multiple users can add or modify tasks simultaneously. Task assignment triggers Firebase Cloud Messaging notifications. Users join a board via a deep link with the board ID.
Real-time collaboration — the Firestore way
Collaboration features require two things: a data model that handles concurrent writes safely, and real-time listeners so all users see changes instantly. Firestore's real-time onSnapshot listeners handle the second part automatically — FlutterFlow's Backend Queries use these listeners by default. The data model is where most developers go wrong. Storing tasks as an array field on the board document seems natural but causes conflicts when two users modify the board at the same time — Firestore's last-write-wins on array fields will silently drop one user's change. The correct approach is a tasks subcollection: each task is its own document, and two users can update different tasks concurrently with no conflicts.
Prerequisites
- FlutterFlow Pro plan (push notifications and deep links require Pro)
- Firebase project with Firestore, Firebase Auth, and Cloud Messaging enabled
- Firebase Cloud Functions deployed (for sending FCM notifications)
- Understanding of FlutterFlow Backend Queries and Action Flows
Step-by-step guide
Design the Firestore data model with subcollections
Design the Firestore data model with subcollections
Create two Firestore collections. The 'boards' collection stores board metadata: name, ownerId, memberIds (array of user UIDs), and createdAt. The 'tasks' subcollection lives under each board document: boards/{boardId}/tasks. Each task document has title, description, status ('todo', 'inprogress', 'done'), assignedToId (user UID), assignedToName (denormalized), sortOrder (integer), and createdAt. Using a subcollection means Firestore handles each task update independently — user A updating task 1 and user B updating task 2 at the same millisecond causes no conflict. Add Firestore security rules that allow read/write only for users listed in the board's memberIds array.
1// Firestore Security Rules2rules_version = '2';3service cloud.firestore {4 match /databases/{database}/documents {5 // Board: readable/writable by members only6 match /boards/{boardId} {7 allow read, write: if request.auth != null8 && request.auth.uid in resource.data.memberIds;910 // Tasks subcollection: same membership check11 match /tasks/{taskId} {12 allow read, write: if request.auth != null13 && request.auth.uid in get(/databases/$(database)/documents/boards/$(boardId)).data.memberIds;14 }15 }16 }17}Expected result: Firestore rules deployed. Only board members can read or write tasks. Non-members receive 'permission-denied' errors.
Create the board and set up real-time task listener in FlutterFlow
Create the board and set up real-time task listener in FlutterFlow
Create a 'Board Detail' page in FlutterFlow. Add a Backend Query at the page level for the specific board document (query by document ID, passed as a page parameter). Add a second Backend Query for the tasks subcollection: path is boards/{boardId}/tasks, ordered by status and then sortOrder, with real-time listening enabled (toggle 'Single Time Query' OFF). This means FlutterFlow subscribes to the subcollection and automatically updates the UI whenever any team member creates, updates, or deletes a task. Build a Kanban column layout: three Columns (Todo, In Progress, Done) each with a filtered ListView showing tasks where status matches.
Expected result: Opening the board page on two different devices (logged in as different users) shows the same task list. Adding a task on one device makes it appear on the other device within 1-2 seconds.
Add task creation and assignment with user picker
Add task creation and assignment with user picker
Add a FloatingActionButton that opens a Bottom Sheet modal for new task creation. The form has a title TextField, description TextField, status dropdown, and a user picker for assignment. For the user picker, add a Backend Query loading all board member profiles (query the 'users' collection by UID, filtered to the board's memberIds). Display members as a horizontal row of avatar chips — tapping one selects that member as the assignee. On form submit, use a Firestore Create Document action to add the task to boards/{boardId}/tasks, storing the selected user's UID and display name as assignedToId and assignedToName.
Expected result: New tasks appear in the correct status column immediately on all connected devices. The assigned user's name shows on the task card.
Send FCM push notifications when a task is assigned
Send FCM push notifications when a task is assigned
When a task is assigned to a user, send them a push notification. This requires a Cloud Function that fires on tasks onCreate and checks if an assignedToId exists. It reads the assigned user's FCM token from their user document, then calls Firebase Admin SDK's messaging().send(). Store FCM tokens in the user document when the app starts: in FlutterFlow's App initialization actions, request notification permission, get the FCM token (use the Get Device Token action), and save it to the user's Firestore document.
1// functions/index.js — Task assignment notification2exports.onTaskAssigned = functions.firestore3 .document('boards/{boardId}/tasks/{taskId}')4 .onCreate(async (snap, context) => {5 const task = snap.data();6 if (!task.assignedToId) return null;78 const userDoc = await admin.firestore()9 .doc(`users/${task.assignedToId}`)10 .get();1112 const fcmToken = userDoc.data()?.fcmToken;13 if (!fcmToken) return null;1415 const message = {16 token: fcmToken,17 notification: {18 title: 'New task assigned to you',19 body: task.title,20 },21 data: {22 boardId: context.params.boardId,23 taskId: context.params.taskId,24 type: 'task_assigned',25 },26 android: { priority: 'high' },27 apns: { payload: { aps: { badge: 1 } } },28 };2930 await admin.messaging().send(message);31 return null;32 });Expected result: When a task is assigned to a user, that user receives a push notification on their device within 5 seconds, even if the app is in the background.
Create a board invite deep link
Create a board invite deep link
Users can invite collaborators by sharing a deep link that opens the app to the specific board and adds the user to the memberIds array. In FlutterFlow, enable Deep Linking in Project Settings → App Details → Deep Linking. Set the deep link format to yourapp://board?id={boardId}. Create a 'Join Board' page that parses the board ID from the incoming deep link URL parameter, shows the board details, and has a 'Join Board' button. The Join button calls a Cloud Function (or Firestore Update) that adds the current user's UID to the board's memberIds array using FieldValue.arrayUnion.
1// Cloud Function: joinBoard2exports.joinBoard = functions.https.onCall(async (data, context) => {3 if (!context.auth) {4 throw new functions.https.HttpsError('unauthenticated', 'Login required');5 }67 const { boardId } = data;8 const userId = context.auth.uid;910 const boardRef = admin.firestore().doc(`boards/${boardId}`);11 const board = await boardRef.get();1213 if (!board.exists) {14 throw new functions.https.HttpsError('not-found', 'Board not found');15 }1617 // Add user to members (arrayUnion avoids duplicates)18 await boardRef.update({19 memberIds: admin.firestore.FieldValue.arrayUnion(userId),20 });2122 return { success: true, boardName: board.data().name };23});Expected result: Sharing the deep link and tapping it on another device opens the app to a Join Board screen. Tapping Join adds the user to the board and redirects them to the task view.
Complete working example
1// Firebase Cloud Functions: Multi-User Collaboration2// Handles task assignment notifications and board invite joining34const functions = require('firebase-functions');5const admin = require('firebase-admin');67admin.initializeApp();89// Notify user when a task is assigned to them10exports.onTaskAssigned = functions.firestore11 .document('boards/{boardId}/tasks/{taskId}')12 .onCreate(async (snap, context) => {13 const task = snap.data();14 if (!task.assignedToId) return null;1516 try {17 const userDoc = await admin.firestore()18 .doc(`users/${task.assignedToId}`).get();19 const { fcmToken, displayName } = userDoc.data() || {};20 if (!fcmToken) return null;2122 await admin.messaging().send({23 token: fcmToken,24 notification: {25 title: 'New task assigned to you',26 body: task.title,27 },28 data: {29 boardId: context.params.boardId,30 taskId: context.params.taskId,31 type: 'task_assigned',32 },33 android: { priority: 'high' },34 apns: { payload: { aps: { badge: 1, sound: 'default' } } },35 });36 } catch (err) {37 console.error('Notification error:', err.message);38 }39 return null;40 });4142// Allow user to join a board via invite deep link43exports.joinBoard = functions.https.onCall(async (data, context) => {44 if (!context.auth) {45 throw new functions.https.HttpsError('unauthenticated', 'Login required');46 }4748 const { boardId } = data;49 if (!boardId) {50 throw new functions.https.HttpsError('invalid-argument', 'boardId is required');51 }5253 const boardRef = admin.firestore().doc(`boards/${boardId}`);54 const board = await boardRef.get();55 if (!board.exists) {56 throw new functions.https.HttpsError('not-found', 'Board not found');57 }5859 const userId = context.auth.uid;60 await boardRef.update({61 memberIds: admin.firestore.FieldValue.arrayUnion(userId),62 });6364 return { success: true, boardName: board.data().name };65});6667// Clean up board when owner deletes it68exports.onBoardDeleted = functions.firestore69 .document('boards/{boardId}')70 .onDelete(async (snap, context) => {71 const tasksSnap = await admin.firestore()72 .collection(`boards/${context.params.boardId}/tasks`)73 .get();7475 const batch = admin.firestore().batch();76 tasksSnap.docs.forEach(doc => batch.delete(doc.ref));77 await batch.commit();78 return null;79 });Common mistakes when implementing Multi-User Collaboration in FlutterFlow
Why it's a problem: Storing tasks as an Array field on the board document instead of a subcollection
How to avoid: Always store tasks as individual documents in a subcollection (boards/{boardId}/tasks). Each task update is independent — concurrent writes to different task documents never conflict.
Why it's a problem: Not denormalizing the assigned user's display name onto the task document
How to avoid: Store both assignedToId and assignedToName on the task at creation time. If the user's display name changes, use a Cloud Function onUpdate trigger to update the denormalized name on all their assigned tasks.
Why it's a problem: Requesting notification permission immediately on app first launch
How to avoid: Request notification permission only when it's contextually relevant — e.g., when the user assigns a task for the first time, show an explanation dialog first: 'Enable notifications to know when tasks are assigned to you.' Then request the permission.
Best practices
- Always use Firestore subcollections for multi-user entities — never arrays of complex objects
- Use FieldValue.arrayUnion and FieldValue.arrayRemove for memberIds to handle concurrent membership changes safely
- Store FCM tokens in user documents and update them on every app launch — tokens can change
- Limit Firestore real-time listeners: use them only on pages where real-time updates are visible and important
- Add Firestore security rules that enforce membership before any task read or write
- Use deep links with expiry tokens rather than raw IDs to limit unauthorized board access
- Paginate task lists with a 50-item limit per column to keep initial load fast on boards with many tasks
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a multi-user Kanban board app in FlutterFlow with Firebase. I need a Firebase Cloud Function that triggers when a new task document is created under the boards/{boardId}/tasks subcollection. If the task has an assignedToId field, the function should fetch that user's FCM token from their user document and send a push notification with the task title as the message body. Include error handling for missing tokens.
In FlutterFlow, I want to build a Kanban board page with three status columns (Todo, In Progress, Done). Each column should show tasks from the boards/{boardId}/tasks subcollection filtered by status, updating in real time. How do I set up three separate Backend Queries (one per column) on the same page, each filtered by a different status value, with real-time listening enabled?
Frequently asked questions
How do I prevent two users from editing the same task simultaneously and overwriting each other?
Use Firestore transactions for task edits. A transaction reads the current document state before writing, and Firestore retries automatically if another write happened in between. For the FlutterFlow UI, show a 'last edited by' timestamp on each task so users know when someone else just changed it.
Can I see which users are currently viewing the board (like Google Docs' colored cursors)?
Yes, with Firebase Realtime Database's presence system. Write to a 'presence/{boardId}/{userId}' path when a user opens the board and delete it on close. Use onDisconnect() to auto-remove the entry if the user loses connection. Display connected users as avatar bubbles at the top of the board.
How many collaborators can a board support?
Firestore has no limit on the number of concurrent readers on a document. Practically, boards work well with up to 50-100 active collaborators. Beyond that, the number of simultaneous real-time listeners and write volume per document becomes a concern. Shard heavy-write boards across multiple Firestore documents if needed.
Does the real-time Firestore listener work when the app is in the background?
No. Firestore real-time listeners only work when the app is in the foreground. Background updates require push notifications (FCM). The combination of real-time listeners (for when the app is open) and FCM notifications (for when it's closed) covers both cases.
How do I let users leave a board they've joined?
Add a Leave Board button that calls a Cloud Function using FieldValue.arrayRemove to remove the user's UID from the board's memberIds array. Also clean up any tasks assigned to the leaving user — either reassign them to the board owner or clear the assignedToId field.
Can I restrict which users can see specific tasks (private tasks within a shared board)?
Yes. Add a 'visibleTo' array field on task documents containing allowed UIDs. Update your Firestore security rules to also check request.auth.uid in resource.data.visibleTo for task reads. In the FlutterFlow Backend Query, this filtering happens automatically via Firestore rules — tasks the user can't see are simply not returned.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation