In FlutterFlow, Firestore IS your cross-device sync engine — data written to Firestore is instantly available on all devices signed in as the same user. The most common mistake is storing user preferences or app data in FlutterFlow's App State, which lives only in device memory. Store everything that should persist or sync in Firestore user documents. Enable offline persistence for smooth offline experiences.
Cross-Device Sync Is Already Built In
Many FlutterFlow developers ask 'how do I sync data across devices?' without realizing the answer is already in their app: any data in Firestore automatically appears on every device where the user is signed in, because Firestore is a cloud database with real-time listeners. The real question is whether you are storing data in the right place. This guide clarifies the three storage tiers in FlutterFlow and shows you exactly which data goes where.
Prerequisites
- FlutterFlow project with Firebase and Firestore configured
- Firebase Authentication set up with user accounts
- Basic understanding of Firestore collections and documents
- A user document in Firestore for each authenticated user
Step-by-step guide
Understand the Three Storage Tiers in FlutterFlow
Understand the Three Storage Tiers in FlutterFlow
FlutterFlow has three places to store data, each with different persistence and sync behavior. App State lives only in device memory — it is reset every time the app restarts and never synchronizes between devices. Page State is even more temporary — it is reset whenever the user navigates away from that page. Firestore is a cloud database — data persists permanently, syncs across all devices in real time, and survives app restarts. The decision rule is simple: if the data should exist tomorrow or on another device, it must go in Firestore. App State is only for temporary UI state that does not need to survive navigation (like whether a dropdown is open, or which tab is selected).
Expected result: You can categorize every piece of data in your app: UI state (App State), temporary form data (Page State), and persistent user data (Firestore).
Move User Preferences from App State to Firestore
Move User Preferences from App State to Firestore
Identify all App State variables that represent user preferences or settings: dark mode toggle, notification preferences, language selection, accessibility settings, default filters, and saved layouts. For each one, add a corresponding field to your users Firestore collection schema in FlutterFlow. On app startup (main page On Load action), fetch the current user document and populate App State from the Firestore values. When the user changes a preference, update both the App State variable (for immediate UI response) and the Firestore user document (for persistence and cross-device sync). This two-write pattern gives instant UI updates while ensuring persistence.
1// Pattern: load user preferences from Firestore on app start2// and save changes back to Firestore when updated3import 'package:cloud_firestore/cloud_firestore.dart';4import 'package:firebase_auth/firebase_auth.dart';56// Load preferences on startup7Future<Map<String, dynamic>> loadUserPreferences() async {8 final uid = FirebaseAuth.instance.currentUser?.uid;9 if (uid == null) return {};1011 final snap = await FirebaseFirestore.instance12 .collection('users')13 .doc(uid)14 .get();1516 final data = snap.data() ?? {};17 return {18 'dark_mode': data['preferences']?['dark_mode'] ?? false,19 'language': data['preferences']?['language'] ?? 'en',20 'notifications_enabled': data['preferences']?['notifications_enabled'] ?? true,21 'default_currency': data['preferences']?['default_currency'] ?? 'USD',22 };23}2425// Save a preference change26Future<void> savePreference(String key, dynamic value) async {27 final uid = FirebaseAuth.instance.currentUser?.uid;28 if (uid == null) return;2930 await FirebaseFirestore.instance31 .collection('users')32 .doc(uid)33 .update({'preferences.$key': value});34}Expected result: User toggles dark mode on their phone, then opens the app on their tablet — dark mode is already on without any additional action.
Enable Firestore Offline Persistence
Enable Firestore Offline Persistence
Firestore offline persistence allows your app to read and write data even without an internet connection. Pending writes queue locally and sync automatically when connectivity returns. In FlutterFlow, enable offline persistence by adding a Custom Action that runs during app initialization. Set PersistenceSettings with the cacheSizeBytes configuration. With persistence enabled, your app's Firestore listeners serve data from the local cache when offline, so the UI never goes blank due to connectivity issues. Data written offline appears in the UI instantly from cache and then syncs to the server when back online.
1import 'package:cloud_firestore/cloud_firestore.dart';23Future<void> enableFirestorePersistence() async {4 try {5 FirebaseFirestore.instance.settings = const Settings(6 persistenceEnabled: true,7 cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,8 );9 } catch (e) {10 // Persistence may already be enabled or not supported on this platform11 // This is a non-fatal error12 print('Firestore persistence setup: $e');13 }14}Expected result: Taking the device offline and using the app continues to work — data displays from cache. When reconnected, any changes made offline sync automatically.
Sync Real-Time Data with Firestore Listeners
Sync Real-Time Data with Firestore Listeners
In FlutterFlow, any widget bound to a Firestore document or collection automatically receives real-time updates via StreamBuilder. When the user's Firestore document changes on one device, the StreamBuilder on all other signed-in devices receives the update within milliseconds and rebuilds the relevant widgets. To wire this up, make sure your preference-displaying widgets (like a dark mode toggle or language selector) read from the Firestore document stream rather than only from App State. Use FlutterFlow's Document reference binding on the current user's document to automatically propagate changes across all devices.
Expected result: Updating a setting on one device is visible on a second device within 1-2 seconds without needing to refresh the app.
Handle Offline Conflicts with Atomic Firestore Operations
Handle Offline Conflicts with Atomic Firestore Operations
When two devices edit the same user data while offline, they may produce conflicting writes that both need to sync when reconnecting. For most user preference data, last-write-wins (Firestore's default) is acceptable — the most recent change wins. But for data like read counts, bookmark lists, or shopping carts, use FieldValue.increment() and FieldValue.arrayUnion() so Firestore can merge the changes atomically rather than overwriting. For example, if the user adds items to a favorites list on two devices while offline, arrayUnion() merges both additions when they reconnect instead of one list overwriting the other.
1import 'package:cloud_firestore/cloud_firestore.dart';2import 'package:firebase_auth/firebase_auth.dart';34// Safe: add to favorites on two offline devices — both additions preserved5Future<void> addToFavorites(String itemId) async {6 final uid = FirebaseAuth.instance.currentUser?.uid;7 if (uid == null) return;89 await FirebaseFirestore.instance10 .collection('users')11 .doc(uid)12 .update({13 'favorites': FieldValue.arrayUnion([itemId]),14 'favorites_count': FieldValue.increment(1),15 });16}1718// Safe: remove from favorites19Future<void> removeFromFavorites(String itemId) async {20 final uid = FirebaseAuth.instance.currentUser?.uid;21 if (uid == null) return;2223 await FirebaseFirestore.instance24 .collection('users')25 .doc(uid)26 .update({27 'favorites': FieldValue.arrayRemove([itemId]),28 'favorites_count': FieldValue.increment(-1),29 });30}Expected result: Adding a favorite on an offline phone and a different favorite on an offline tablet — both appear in the favorites list after reconnecting, not just one.
Complete working example
1// Complete cross-device sync setup for FlutterFlow2// Include these Custom Actions in your app initialization flow34import 'package:cloud_firestore/cloud_firestore.dart';5import 'package:firebase_auth/firebase_auth.dart';67// ─── Step 1: Enable offline persistence (call before any Firestore use) ───────89void enableFirestorePersistence() {10 try {11 FirebaseFirestore.instance.settings = const Settings(12 persistenceEnabled: true,13 cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,14 );15 } catch (_) {}16}1718// ─── Step 2: Load user preferences on login ───────────────────────────────────1920Future<Map<String, dynamic>> loadUserPreferences() async {21 final uid = FirebaseAuth.instance.currentUser?.uid;22 if (uid == null) return {};2324 final snap = await FirebaseFirestore.instance25 .collection('users').doc(uid).get();2627 final prefs = (snap.data()?['preferences'] as Map<String, dynamic>?) ?? {};28 return {29 'dark_mode': prefs['dark_mode'] ?? false,30 'language': prefs['language'] ?? 'en',31 'notifications_enabled': prefs['notifications_enabled'] ?? true,32 'default_currency': prefs['default_currency'] ?? 'USD',33 'onboarding_complete': snap.data()?['onboarding_complete'] ?? false,34 };35}3637// ─── Step 3: Save individual preference changes ───────────────────────────────3839Future<bool> savePreference(String key, dynamic value) async {40 final uid = FirebaseAuth.instance.currentUser?.uid;41 if (uid == null) return false;42 try {43 await FirebaseFirestore.instance44 .collection('users').doc(uid)45 .update({'preferences.$key': value});46 return true;47 } catch (_) {48 return false;49 }50}5152// ─── Step 4: Atomic list operations (conflict-safe) ──────────────────────────5354Future<void> addToList(String field, dynamic item) async {55 final uid = FirebaseAuth.instance.currentUser?.uid;56 if (uid == null) return;57 await FirebaseFirestore.instance.collection('users').doc(uid)58 .update({field: FieldValue.arrayUnion([item])});59}6061Future<void> removeFromList(String field, dynamic item) async {62 final uid = FirebaseAuth.instance.currentUser?.uid;63 if (uid == null) return;64 await FirebaseFirestore.instance.collection('users').doc(uid)65 .update({field: FieldValue.arrayRemove([item])});66}6768// ─── Step 5: Subscribe to real-time user document updates ─────────────────────6970Stream<Map<String, dynamic>> userPreferencesStream(String uid) {71 return FirebaseFirestore.instance72 .collection('users').doc(uid)73 .snapshots()74 .map((snap) => (snap.data()?['preferences'] as Map<String, dynamic>?) ?? {});75}Common mistakes
Why it's a problem: Storing user preferences in App State instead of Firestore
How to avoid: Store all preferences in the user's Firestore document. Load them into App State on app startup for fast access, and write changes back to Firestore so they persist and sync.
Why it's a problem: Using set() with full preference object instead of nested dot-notation update
How to avoid: Use update() with dot notation keys like 'preferences.dark_mode' to update only the specific field that changed, leaving all other preference fields untouched.
Why it's a problem: Not initializing Firestore offline persistence before the first Firestore read
How to avoid: Call enableFirestorePersistence() in main.dart before runApp(), or in the very first Custom Action in your app initialization flow, before any Firestore reads.
Best practices
- Use a nested 'preferences' map in the user document rather than top-level fields to keep the document organized and easy to migrate.
- Always load preferences on app startup and populate App State — do not fetch from Firestore on every widget build.
- Use FieldValue.arrayUnion() and FieldValue.arrayRemove() for list-type preferences so offline changes from multiple devices merge correctly.
- Listen to the user document as a real-time stream so that changes made on one device appear on all others without requiring app restart.
- Set a reasonable cache size for offline persistence — CACHE_SIZE_UNLIMITED is fine for most apps with modest data volumes, but set a cap (e.g., 100MB) for apps with large media metadata.
- Migrate preferences from App State to Firestore gradually: add the Firestore read/write but keep the App State variable as a local mirror for backward compatibility.
- Test cross-device sync explicitly: log in on two devices simultaneously and verify that a preference change on one appears on the other within a few seconds.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app with Firebase. Explain how Firestore provides automatic cross-device sync, which data should be in Firestore versus App State versus Page State, how to load and save user preferences from a Firestore user document, how to enable offline persistence, and how to use FieldValue.arrayUnion to handle offline conflict resolution for list-type data. Show complete Dart code.
In my FlutterFlow app, add an On Load action to my main app page that calls a Custom Action to load the current user's preferences from their Firestore document and sets App State variables: darkMode (bool), selectedLanguage (String), and notificationsEnabled (bool). Also add a Custom Action called savePreference(key, value) that updates a specific preference field using dot notation.
Frequently asked questions
Does Firestore sync data between devices instantly?
Firestore updates propagate to active listeners typically within 100-300 milliseconds on a good connection. Both devices must have an active real-time listener (StreamBuilder) on the same document for updates to appear immediately. If the second device opens the app later, it reads the latest data from Firestore on next query — no streaming required.
What happens to App State data when the user logs out?
App State is not tied to authentication — it persists for the duration of the app session regardless of who is logged in. If you store user-specific data in App State and the user logs out, that data remains in App State until the app restarts or until you explicitly clear it. Always clear App State variables when a user signs out by adding a Clear App State action to your sign-out flow.
How do I sync data for a user who uses the app without an account?
Use Firebase anonymous authentication. Sign users in anonymously on first launch and create a Firestore user document with their anonymous UID. When they later create a full account, link the anonymous account to the email/password account using linkWithCredential() — the UID stays the same and all their Firestore data remains attached. This gives you sync across anonymous sessions on the same device and an upgrade path to cross-device sync once they register.
What data should I never put in Firestore?
Never put sensitive credentials (API keys, passwords) in Firestore documents. Never store large binary files (images, videos, audio) as Base64 strings — use Firebase Storage instead and store the URL. Never store data that changes more than once per second per document — use Firestore for infrequent updates and consider Realtime Database for high-frequency counters.
How do I sync data that users should not be able to see on each other's devices?
Use Firestore Security Rules to restrict each user's data to only their own documents. The standard rule pattern is: allow read, write: if request.auth.uid == resource.data.user_id. This ensures users can only read and write documents where the user_id field matches their own authenticated UID, so cross-user data never leaks.
My users report that their data disappeared after reinstalling the app. What happened?
Reinstalling the app clears the local Firestore offline cache but does not affect the cloud data. The likely cause is that the data was stored in App State (which is cleared on reinstall) rather than Firestore. If the data genuinely exists in Firestore, it will reload the next time the user logs in and the relevant queries run. Verify this by checking the user's Firestore documents directly in Firebase Console.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation