Real-time collaboration in FlutterFlow is built on Firestore real-time listeners. Use a presence system with an active_users subcollection for who-is-online tracking, Firestore FieldValue atomic operations to prevent concurrent write conflicts, a cursors subcollection for live cursor positions, and an activity_feed collection for the audit trail. Never use set() with full document data for collaborative edits.
Multiple Users, One Document, No Conflicts
Real-time collaboration requires more than just Firestore real-time listeners. You need three systems working together: a presence layer so users know who else is active, atomic write operations so two simultaneous saves do not overwrite each other, and an activity log so users can understand what changed and by whom. This guide builds all three patterns in FlutterFlow using Firestore as the foundation, without any third-party real-time services.
Prerequisites
- FlutterFlow project with Firebase and Firestore configured
- Basic Firestore queries and real-time listeners working in your project
- Authentication set up so you have a current user ID available
- FlutterFlow Pro plan recommended for Custom Actions
Step-by-step guide
Build the Presence System with Active Users Tracking
Build the Presence System with Active Users Tracking
Create an active_users subcollection on every collaborative document. Each document in active_users uses the user's UID as the document ID and contains display_name, avatar_url, last_seen (timestamp), and cursor_position (map). When a user opens a collaborative document, write their presence document with the current timestamp. Use a timer-based Custom Action to refresh last_seen every 30 seconds. On page dispose or app background, delete the user's presence document. In FlutterFlow, add a StreamBuilder on the active_users subcollection to display live avatar bubbles for all currently active users — filter to only show users with last_seen within the last 60 seconds.
1import 'package:cloud_firestore/cloud_firestore.dart';2import 'package:firebase_auth/firebase_auth.dart';34Future<void> joinDocumentSession(String documentId) async {5 final uid = FirebaseAuth.instance.currentUser?.uid;6 final user = FirebaseAuth.instance.currentUser;7 if (uid == null) return;89 final presenceRef = FirebaseFirestore.instance10 .collection('documents')11 .doc(documentId)12 .collection('active_users')13 .doc(uid);1415 await presenceRef.set({16 'uid': uid,17 'display_name': user?.displayName ?? 'Anonymous',18 'avatar_url': user?.photoURL ?? '',19 'joined_at': FieldValue.serverTimestamp(),20 'last_seen': FieldValue.serverTimestamp(),21 'cursor_position': null,22 });23}2425Future<void> leaveDocumentSession(String documentId) async {26 final uid = FirebaseAuth.instance.currentUser?.uid;27 if (uid == null) return;2829 await FirebaseFirestore.instance30 .collection('documents')31 .doc(documentId)32 .collection('active_users')33 .doc(uid)34 .delete();35}Expected result: When two users open the same document, both see the other's avatar in the collaborators bar at the top of the page. Avatars disappear within 60 seconds of a user closing the document.
Use Firestore Atomic Operations to Prevent Write Conflicts
Use Firestore Atomic Operations to Prevent Write Conflicts
When multiple users edit a document simultaneously, naive full-document writes (using set() or update() with the whole object) will overwrite each other's changes. Instead, use FieldValue.increment() for counters, FieldValue.arrayUnion() and FieldValue.arrayRemove() for list operations, and Firestore transactions for multi-field updates that must be consistent. For text fields where true merging is needed (like collaborative text editing), implement a last-write-wins strategy with a version counter — only accept a write if the client's version matches the current document version, otherwise show a conflict resolution UI.
1import 'package:cloud_firestore/cloud_firestore.dart';23// Safe: atomic increment — no conflict possible4Future<void> incrementViewCount(String documentId) async {5 await FirebaseFirestore.instance6 .collection('documents')7 .doc(documentId)8 .update({'view_count': FieldValue.increment(1)});9}1011// Safe: add a collaborator without touching other fields12Future<void> addCollaborator(String documentId, String userId) async {13 await FirebaseFirestore.instance14 .collection('documents')15 .doc(documentId)16 .update({'collaborator_ids': FieldValue.arrayUnion([userId])});17}1819// Versioned update — prevents overwrite if document changed since last read20Future<bool> updateDocumentContent(21 String documentId,22 String newContent,23 int expectedVersion,24) async {25 final docRef = FirebaseFirestore.instance26 .collection('documents')27 .doc(documentId);2829 bool success = false;30 await FirebaseFirestore.instance.runTransaction((transaction) async {31 final snapshot = await transaction.get(docRef);32 final currentVersion = snapshot.data()?['version'] ?? 0;3334 if (currentVersion != expectedVersion) {35 // Conflict — another user has edited since we last read36 success = false;37 return;38 }3940 transaction.update(docRef, {41 'content': newContent,42 'version': FieldValue.increment(1),43 'last_edited_by': FirebaseAuth.instance.currentUser?.uid,44 'last_edited_at': FieldValue.serverTimestamp(),45 });46 success = true;47 });48 return success;49}Expected result: Two users can add themselves as collaborators simultaneously without either overwriting the other. Version conflicts show a 'Document was updated — please refresh' message.
Broadcast Live Cursor and Selection Positions
Broadcast Live Cursor and Selection Positions
For richer collaboration, show where other users are focused in the document. Create a Custom Action called updateCursorPosition that writes the current user's scroll position or selected field name to their active_users document. Keep this update debounced to at most once every 500ms to avoid write storms. In FlutterFlow, listen to the active_users stream and render a small colored badge showing each collaborator's name next to the field or section they are currently editing. Assign each user a consistent color based on their UID (use a hash to pick from a palette) so their indicator color stays stable across sessions.
Expected result: When User A clicks on the Title field, User B sees a small colored 'Sarah' badge appear next to the Title field in their own view.
Build the Activity Feed Audit Trail
Build the Activity Feed Audit Trail
Create an activity_feed subcollection on each document. Every significant change writes an activity document with: action (String — 'edited_title', 'added_collaborator', 'changed_status'), actor_id (String), actor_name (String), timestamp (server timestamp), and a changes map with before and after values for auditable fields. Write activity entries inside your Firestore transactions so the audit record is atomic with the change itself. In FlutterFlow, add a StreamBuilder on activity_feed ordered by timestamp descending to show a live 'What happened recently' list below the document.
1Future<void> logActivity(2 String documentId,3 String action,4 Map<String, dynamic> changes,5) async {6 final user = FirebaseAuth.instance.currentUser;7 if (user == null) return;89 await FirebaseFirestore.instance10 .collection('documents')11 .doc(documentId)12 .collection('activity_feed')13 .add({14 'action': action,15 'actor_id': user.uid,16 'actor_name': user.displayName ?? 'Unknown',17 'actor_avatar': user.photoURL ?? '',18 'timestamp': FieldValue.serverTimestamp(),19 'changes': changes,20 });21}Expected result: The activity feed updates in real time showing entries like 'Sarah changed title from Draft to Final Report 2 minutes ago'.
Handle Offline Editing and Conflict Resolution UI
Handle Offline Editing and Conflict Resolution UI
Firestore has built-in offline persistence — pending writes are queued locally and synced when connectivity returns. Enable persistence in your Firebase initialization if not already active. For the collaboration conflict case, when your versioned update returns false (conflict detected), store the conflicting content in a Page State variable and show a bottom sheet with a side-by-side comparison: the user's version and the current server version. Let the user choose which content to keep, or merge manually. This prevents silent data loss in high-conflict scenarios.
Expected result: When a write conflict occurs, a bottom sheet slides up showing 'Your version' vs 'Latest version' with Keep Mine and Use Latest buttons.
Complete working example
1import 'package:cloud_firestore/cloud_firestore.dart';2import 'package:firebase_auth/firebase_auth.dart';34class CollaborationService {5 final FirebaseFirestore _db = FirebaseFirestore.instance;67 User? get _user => FirebaseAuth.instance.currentUser;89 // ─── Presence ───────────────────────────────────────────────────────────────1011 Future<void> joinSession(String documentId) async {12 if (_user == null) return;13 await _presenceRef(documentId).set({14 'uid': _user!.uid,15 'display_name': _user!.displayName ?? 'Anonymous',16 'avatar_url': _user!.photoURL ?? '',17 'last_seen': FieldValue.serverTimestamp(),18 });19 }2021 Future<void> leaveSession(String documentId) async {22 if (_user == null) return;23 await _presenceRef(documentId).delete();24 }2526 Future<void> pingSession(String documentId) async {27 if (_user == null) return;28 await _presenceRef(documentId)29 .update({'last_seen': FieldValue.serverTimestamp()});30 }3132 Stream<List<Map<String, dynamic>>> activeUsersStream(String documentId) {33 final cutoff = DateTime.now().subtract(const Duration(seconds: 90));34 return _db35 .collection('documents')36 .doc(documentId)37 .collection('active_users')38 .snapshots()39 .map((snap) => snap.docs40 .where((d) {41 final ts = d.data()['last_seen'] as Timestamp?;42 return ts != null && ts.toDate().isAfter(cutoff);43 })44 .map((d) => d.data())45 .toList());46 }4748 // ─── Atomic Content Update ────────────────────────────────────────────────4950 Future<bool> updateContent(51 String documentId,52 String field,53 dynamic value,54 int expectedVersion,55 ) async {56 final docRef = _db.collection('documents').doc(documentId);57 bool success = false;5859 await _db.runTransaction((tx) async {60 final snap = await tx.get(docRef);61 final currentVersion = (snap.data()?['version'] ?? 0) as int;6263 if (currentVersion != expectedVersion) {64 success = false;65 return;66 }6768 tx.update(docRef, {69 field: value,70 'version': FieldValue.increment(1),71 'updated_by': _user?.uid,72 'updated_at': FieldValue.serverTimestamp(),73 });74 success = true;75 });7677 if (success) {78 await _logActivity(documentId, 'updated_$field', {field: value});79 }80 return success;81 }8283 // ─── Activity Log ─────────────────────────────────────────────────────────8485 Future<void> _logActivity(86 String documentId,87 String action,88 Map<String, dynamic> changes,89 ) async {90 await _db91 .collection('documents')92 .doc(documentId)93 .collection('activity_feed')94 .add({95 'action': action,96 'actor_id': _user?.uid,97 'actor_name': _user?.displayName ?? 'Unknown',98 'timestamp': FieldValue.serverTimestamp(),99 'changes': changes,100 });101 }102103 DocumentReference _presenceRef(String documentId) => _db104 .collection('documents')105 .doc(documentId)106 .collection('active_users')107 .doc(_user?.uid);108}Common mistakes
Why it's a problem: Using set() or update() with full document data for every keystroke
How to avoid: Use FieldValue atomic operations for independent fields, debounce text updates to at most once every 1-2 seconds, and use versioned transactions for content that multiple users might edit at the same time.
Why it's a problem: Never cleaning up the presence document when a user leaves
How to avoid: Delete the presence document in your page's dispose callback, in the App's lifecycle listener for background state, and set a Firestore Cloud Function trigger to clean up documents where last_seen is more than 5 minutes old.
Why it's a problem: Streaming all activity feed history on every page load
How to avoid: Use a limit(50) query ordered by timestamp descending. Add a Load More button that uses startAfterDocument for pagination rather than loading everything at once.
Best practices
- Assign collaborator colors deterministically from UID — use a hash to pick from a fixed palette so the same user always appears in the same color across sessions.
- Debounce all position and cursor updates to at most once per 500ms to keep Firestore write costs manageable in active sessions.
- Use Firebase Realtime Database instead of Firestore for the presence layer — RTDB's onDisconnect() is more reliable for cleanup than Firestore dispose callbacks.
- Add a document version counter and increment it on every significant edit so all clients know when to re-fetch.
- Limit active_users cleanup to a Cloud Function on a schedule rather than relying solely on client-side dispose — app crashes skip dispose.
- Show a 'Last saved by [name] X minutes ago' indicator so users always know the current save state without needing the full activity feed.
- For text-heavy collaborative content, consider using a CRDT library (like crdt package) instead of last-write-wins for truly conflict-free merging.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a real-time collaborative document editor in FlutterFlow with Firebase. Explain how to build a presence system tracking who is online, how to prevent write conflicts using Firestore atomic operations and versioned transactions, how to broadcast cursor positions across users, and how to build an activity feed. Show all Dart code.
In my FlutterFlow app, add a Custom Action called joinDocumentSession that writes the current user's UID, display name, and a server timestamp to an active_users subcollection on a given document. Also add a leaveDocumentSession action that deletes the same document. Wire joinDocumentSession to the page's On Load action and leaveDocumentSession to a close button's action flow.
Frequently asked questions
Can Firestore handle the write volume from real-time collaboration?
Yes, with appropriate design. Firestore can handle thousands of writes per second per database. The key is debouncing: instead of writing on every keystroke, batch updates to once per 1-2 seconds. With 50 concurrent collaborators each writing once per second, you are well within Firestore limits and well under the 1 write/second per document constraint if you spread writes across per-user documents rather than one shared document.
How do I handle collaboration when users are offline?
Enable Firestore offline persistence in Firebase initialization. Writes made offline are queued locally and sync automatically when the user reconnects. For conflict resolution in offline scenarios, your version counter approach will detect conflicts when the offline queue syncs — present the user with a merge UI if their offline changes conflict with remote changes made while they were disconnected.
What is the maximum number of concurrent collaborators Firestore can support?
There is no hard limit on concurrent listeners per document. The practical constraint is that a Firestore document can only be written once per second. For the document content itself, use one-second debouncing per user. For presence, each user writes to their own presence document (no contention). For activity feed, each action is a separate document write. Realistically, 50-100 concurrent collaborators on one document is achievable without hitting limits.
Do I need operational transformation (OT) like Google Docs uses?
For most FlutterFlow apps, no. OT is needed only when multiple users are typing into the exact same text field simultaneously and you want character-level merging. For field-level collaboration (one user edits the title, another edits the description), versioned writes with a conflict resolution UI are sufficient and much simpler to implement.
How do I show collaborators working in a different section of the app?
Add a current_page or current_section field to each user's presence document and update it on every page navigation. Listen to all active_users documents and group their avatars by section. Show a 'Sarah and Tom are editing the Introduction section' indicator. This gives users spatial awareness without full cursor broadcasting.
Can I restrict which users can edit vs only view a collaborative document?
Yes. Add a permissions map to the document with user IDs mapped to 'editor' or 'viewer' roles. In Firestore Security Rules, check that the requesting user's ID exists in the permissions map with the 'editor' role before allowing write operations. In FlutterFlow, check the current user's role in Page State and disable edit UI elements for viewers.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation