# How to Implement Multi-User Collaboration in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 60-80 min
- Compatibility: FlutterFlow Pro+ (FCM notifications and deep links require Pro)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

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

```
// Firestore Security Rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Board: readable/writable by members only
    match /boards/{boardId} {
      allow read, write: if request.auth != null
        && request.auth.uid in resource.data.memberIds;

      // Tasks subcollection: same membership check
      match /tasks/{taskId} {
        allow read, write: if request.auth != null
          && request.auth.uid in get(/databases/$(database)/documents/boards/$(boardId)).data.memberIds;
      }
    }
  }
}
```

**Expected result:** Firestore rules deployed. Only board members can read or write tasks. Non-members receive 'permission-denied' errors.

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

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

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

```
// functions/index.js — Task assignment notification
exports.onTaskAssigned = functions.firestore
  .document('boards/{boardId}/tasks/{taskId}')
  .onCreate(async (snap, context) => {
    const task = snap.data();
    if (!task.assignedToId) return null;

    const userDoc = await admin.firestore()
      .doc(`users/${task.assignedToId}`)
      .get();

    const fcmToken = userDoc.data()?.fcmToken;
    if (!fcmToken) return null;

    const message = {
      token: fcmToken,
      notification: {
        title: 'New task assigned to you',
        body: task.title,
      },
      data: {
        boardId: context.params.boardId,
        taskId: context.params.taskId,
        type: 'task_assigned',
      },
      android: { priority: 'high' },
      apns: { payload: { aps: { badge: 1 } } },
    };

    await admin.messaging().send(message);
    return null;
  });
```

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

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

```
// Cloud Function: joinBoard
exports.joinBoard = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { boardId } = data;
  const userId = context.auth.uid;

  const boardRef = admin.firestore().doc(`boards/${boardId}`);
  const board = await boardRef.get();

  if (!board.exists) {
    throw new functions.https.HttpsError('not-found', 'Board not found');
  }

  // Add user to members (arrayUnion avoids duplicates)
  await boardRef.update({
    memberIds: admin.firestore.FieldValue.arrayUnion(userId),
  });

  return { success: true, boardName: board.data().name };
});
```

**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 code example

File: `collaboration_functions.js`

```javascript
// Firebase Cloud Functions: Multi-User Collaboration
// Handles task assignment notifications and board invite joining

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

admin.initializeApp();

// Notify user when a task is assigned to them
exports.onTaskAssigned = functions.firestore
  .document('boards/{boardId}/tasks/{taskId}')
  .onCreate(async (snap, context) => {
    const task = snap.data();
    if (!task.assignedToId) return null;

    try {
      const userDoc = await admin.firestore()
        .doc(`users/${task.assignedToId}`).get();
      const { fcmToken, displayName } = userDoc.data() || {};
      if (!fcmToken) return null;

      await admin.messaging().send({
        token: fcmToken,
        notification: {
          title: 'New task assigned to you',
          body: task.title,
        },
        data: {
          boardId: context.params.boardId,
          taskId: context.params.taskId,
          type: 'task_assigned',
        },
        android: { priority: 'high' },
        apns: { payload: { aps: { badge: 1, sound: 'default' } } },
      });
    } catch (err) {
      console.error('Notification error:', err.message);
    }
    return null;
  });

// Allow user to join a board via invite deep link
exports.joinBoard = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { boardId } = data;
  if (!boardId) {
    throw new functions.https.HttpsError('invalid-argument', 'boardId is required');
  }

  const boardRef = admin.firestore().doc(`boards/${boardId}`);
  const board = await boardRef.get();
  if (!board.exists) {
    throw new functions.https.HttpsError('not-found', 'Board not found');
  }

  const userId = context.auth.uid;
  await boardRef.update({
    memberIds: admin.firestore.FieldValue.arrayUnion(userId),
  });

  return { success: true, boardName: board.data().name };
});

// Clean up board when owner deletes it
exports.onBoardDeleted = functions.firestore
  .document('boards/{boardId}')
  .onDelete(async (snap, context) => {
    const tasksSnap = await admin.firestore()
      .collection(`boards/${context.params.boardId}/tasks`)
      .get();

    const batch = admin.firestore().batch();
    tasksSnap.docs.forEach(doc => batch.delete(doc.ref));
    await batch.commit();
    return null;
  });
```

## Common mistakes

- **Storing tasks as an Array field on the board document instead of a subcollection** — Firestore uses last-write-wins semantics for field updates. If user A and user B both update the tasks array at the same moment (e.g., both add a task), one update overwrites the other and one task is silently lost. This is a data integrity problem that worsens under load. Fix: 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.
- **Not denormalizing the assigned user's display name onto the task document** — If you only store assignedToId (UID) on the task, displaying the user's name requires a second Firestore read per task card — making a list of 20 tasks generate 20 extra reads. Fix: 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.
- **Requesting notification permission immediately on app first launch** — iOS and Android show a system permission dialog the first time you request notifications. Showing it immediately on first launch (before the user understands the app's value) results in high denial rates — often 60-70% of users deny it. Fix: 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

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

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-a-multi-user-collaboration-feature-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-a-multi-user-collaboration-feature-in-flutterflow
