FlutterFlow Security Rules: Complete Beginner's Guide to Protecting Firebase Firestore Data
FlutterFlow Limitations & Workarounds
A Plain-Language Guide for Beginners
Introduction
FlutterFlow is a powerful visual app-builder on top of Flutter. You can build screens, connect to databases, and publish to both Android and iOS — all without writing most of the code yourself. But like every tool, it has boundaries.
This guide explains the most important real-world limitations you will run into as you build more complex apps. For each one, we explain the problem in plain language and give you the exact steps to work around it. No unnecessary detail just what you need to know.
Who is this for?
Developers at any skill level who are using FlutterFlow and want to understand its limits before they get stuck. Basic familiarity with FlutterFlow is assumed, but no advanced coding knowledge is required.
Limitations & Workarounds
The 12 limitations below are ordered by how often you are likely to encounter them in real projects. The most commonly hit issues come first.
1. Firebase Security Rules
⚠ The Problem
✔ The Fix
FlutterFlow's built-in database access controls are designed for simple applications and basic permission management. They are not intended to handle complex, multi-level authorization scenarios such as role-based access, field-level validation, or advanced business logic, which can leave production applications with limited security.
Use FlutterFlow's built-in access controls for straightforward apps and prototypes. For production applications with advanced authorization requirements, implement custom Firebase Security Rules or backend logic to enforce secure, server-side access control and validation.
Built-in Access Controls: FlutterFlow provides four basic access control levels for database queries:
Everyone: Public access.
Authenticated Users: Any logged-in user.
Tagged Users: Access granted only if the user’s ID matches a reference field in the document.
No One: Completely restricted.
Limitation of Tagged Users: This works well for simple ownership (e.g., a user viewing their own profile), but it only works when user references are stored directly in document fields. It is not suitable for complex role-based access control (RBAC).
When You Still Need Firebase Security Rules: You must write custom rules in the Firebase Console for scenarios like:
Admin/Manager roles
Multi-tenant applications
Subscription-based permissions
Department-based access
Complex ownership validation
Part A — Writing Rules in the Firebase Console
Go to console.firebase.google.com and open your project.
Click Firestore Database in the left sidebar, then select the Rules tab.
Replace the default open rules with rules that protect your data. Example:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
// Only the logged-in user can read/write their own document
allow read, write: if request.auth.uid == userId;
}
}
}
Click Publish. Rules go live within seconds.
Important
The default Firebase rule allows everyone to read and write. Always replace it before releasing your app to real users.
Part B — Calling a Cloud Function Before Login
If you need a Cloud Function to run before the user logs in (e.g. sign-up validation), you cannot require Firebase Auth on that function. Leaving it open is a risk. The solution is a shared secret key.
If you need to call a Supabase Edge Function before login, the SDK cannot attach a session token. Use the same http + secret key approach. On the Edge Function side, read the key with Deno.env.get('APP_SECRET').
2. Built-In API Calls Have a ~10 Second Timeout
⚠ The Problem
✔ The Fix
FlutterFlow's built-in API Manager automatically stops requests that run for longer than approximately 10 seconds. Long-running operations such as AI content generation, Supabase Edge Functions, report generation, or other backend tasks can exceed this limit, causing the request to fail even though the server successfully completes the work.
Replace the built-in API call with a Custom Action. When calling Supabase Edge Functions, use the Supabase SDK's .functions.invoke() method instead. It automatically reuses the authenticated user session and allows you to configure a much longer timeout, making it suitable for AI workflows and other long-running backend operations.
Calling a Supabase Edge Function via Custom Action
Never hardcode your Supabase URL or anon key inside a Custom Action. Use the SDK that FlutterFlow already manages for you:
Supabase Edge Functions with moderate logic: 30 s. AI generation (OpenAI etc.): 60–90 s. Heavy reports: 120 s. Always show a loading indicator so the user knows the app is working.
Showing a Loading Indicator
Add a Page State variable: isLoading (Boolean, default: false).
Add a CircularProgressIndicator. Set Conditional Visibility to: isLoading = true.
In your Action Flow — before the Custom Action, set isLoading = true. After it completes, set isLoading = false.
3. Some Packages Need Native File Configuration
⚠ The Problem
✔ The Fix
Many Flutter packages, particularly those for Bluetooth, background services, location, camera, and hardware sensors, require changes to native platform files such as AndroidManifest.xml, Info.plist, or build.gradle. Older tutorials recommend downloading the Flutter project to edit these files manually, making the process slower and more complex.
Starting with FlutterFlow 6.0 (2025), you can edit native platform files directly within FlutterFlow. Files including AndroidManifest.xml, Info.plist, Entitlements.plist, build.gradle, AppDelegate.swift, and others are accessible from the editor, eliminating the need to export and modify the source code for most native configuration tasks.
Step 1 — Add the Package
In FlutterFlow, go to Settings (the < / > icon in the sidebar).
Select the Project Dependencies tab.
Click Add Dependency, enter the package name and version from pub.dev, and click Save.
Step 2 — Use the Permissions Panel First
For common permissions (camera, microphone, location, Bluetooth, notifications) — use the built-in Permissions panel. It writes the correct entries in both Android and iOS files automatically.
Go to FlutterFlow Settings > Permissions.
Toggle ON the permissions your package needs.
Done — no manual file editing required for standard permissions.
Step 3 — Edit Config Files Directly in FlutterFlow
For anything beyond standard permissions (e.g. registering a background service, adding a custom metadata tag), edit the native file directly inside FlutterFlow:
Go to Custom Code > Configuration Files.
Select the file — e.g. AndroidManifest.xml or Info.plist.
Use Snippet mode (recommended): paste only the lines you want to add. FlutterFlow injects them in the correct place without breaking its own required settings.
AndroidManifest.xml · build.gradle · ProGuard files · Info.plist · Entitlements.plist · AppDelegate.swift · main.dart. Downloading the code is only needed for extremely rare edge cases that none of these files cover.
4. Complex Business Logic is Hard to Build Visually
⚠
The Problem
The Action Flow Editor is great for simple tasks — navigate, call API,
update a variable. But it has no loops, no reusable functions, and deeply
nested conditions become a maintenance nightmare.
There is no "for each item in list, do X" in the visual editor.
✔
The Fix
Write the logic as a Custom Action in Dart. You can loop,
use try/catch, call APIs, transform data, and do anything Flutter supports.
Call the action from any button, page load, or timer.
When to Use a Custom Action
You need a loop — processing every item in a list
You need reuse — same logic is used on multiple pages
You need error handling — try/catch is not available in the visual editor
Your condition tree has more than 3 levels — it becomes unreadable visually
How to Create a Custom Action
Go to Custom Code > Custom Actions > Add Action.
Name it, set the return type, and add input parameters.
Write your Dart logic. Short example — looping through a list:
for (final p in prices) { subtotal += p; } // loop — not possible visually
return subtotal * (1 + taxRate);
}
Click Compile, then Save.
In the Action Flow Editor, add the Custom Action and map your parameters.
After the action, add a Conditional Action to handle success or error.
5. State Management Gets Messy in Larger Apps
⚠
The Problem
As your app grows, managing data through App State and Page State variables
gets difficult. There is no support for BLoC, Riverpod, or Provider
patterns.
Keeping multiple screens in sync with the same data can cause bugs that are
hard to trace visually.
✔
The Fix
Use the right state level for each situation. This alone prevents most
state bugs.
State Type
Use When
Example
Component State
Data inside one widget only
Dropdown open/closed
Page State
Data for one screen visit
Selected tab index
App State
Data shared across pages
Logged-in user's name
Auth User
Current session data
User ID, email, role
Practical Tips
Always reset page state on exit — add a clear action when leaving a page to avoid stale values on re-entry
Enable Real-Time on queries — instead of polling, toggle Real-Time in your Supabase or Firestore backend query so the list auto-updates
Keep App State minimal — only store data that multiple pages genuinely nee
6. No Try/Catch Error Handling in the Visual Editor
⚠
The Problem
The Action Flow Editor has no try/catch mechanism. If an API
call or database query fails, you cannot cleanly catch the error and show
the user a helpful message.
The app may freeze or show a raw error string.
✔
The Fix
Write operations that can fail as Custom Actions with Dart
try/catch.
Return a consistent string — 'success' or
'error:message' — and check it with a Conditional Action
in the Flow.
The Pattern
Future<String> doRiskyThing(String input) async {
try {
final result = await someAsyncCall(input);
return result; // success — return actual data
} on TimeoutException {
return 'error:Request timed out.';
} catch (e) {
return 'error:${e.toString()}';
}
}
In the Action Flow after this Custom Action: IF result starts with 'error' → show Snackbar with the message. ELSE → proceed normally.
7. Version Control and Collaboration is Limited
⚠
The Problem
FlutterFlow has no native Git integration. You cannot do
feature branches, pull requests, code reviews, or rollback to a specific
past commit the way you would in a normal code project.
✔
The Fix
Use FlutterFlow's GitHub Sync feature to push your
generated code to GitHub after every significant change.
Combine it with FlutterFlow Branches for feature
isolation.
Setting Up GitHub Sync
Go to FlutterFlow Settings > Integrations > GitHub.
Click Connect and authorise FlutterFlow to access your GitHub account.
Select or create a repository.
Click Push to GitHub. Each push creates a timestamped commit — your history is now on GitHub.
After every significant set of changes, push again.
Team workflow tip
Assign one person to manage FlutterFlow pushes. Other team members review the generated code on GitHub. Use FlutterFlow Branches for new features — build in isolation, then merge when ready.
📷 Screenshot — FlutterFlow Settings > Integrations panel with the GitHub Connect button visible. Circle the button.
[ Insert screenshot here ]
8. Cannot Store Files or Binary Data in Persisted App State
⚠
The Problem
Persisted App State in FlutterFlow only supports primitive types:
String, Integer, and Boolean.
You cannot persist a file, raw bytes (Uint8List), or an
audio recording directly. If you try, FlutterFlow will not allow it.
✔
The Fix
Convert the binary data to a Base64 string using Dart's
built-in dart:convert library.
Store the string in any persisted String variable and decode it back when
you need it.
Encode to Store, Decode to Use
import 'dart:convert';
import 'dart:typed_data';
// Save: convert bytes → Base64 string → store in persisted App State
In App State, add a variable: savedRecordingBase64 | Type: String | Persisted: ON
After recording completes, call encodeToBase64 with the audio bytes.
Save the returned string to savedRecordingBase64.
On next app open, call decodeFromBase64 with savedRecordingBase64 to get the bytes back.
Pass the bytes to your audio player widget.
File size warning
Base64 inflates data size by ~33%. SharedPreferences (which FlutterFlow uses for persisted state) is designed for small values. For files larger than a few hundred KB, upload to Supabase Storage or Firebase Storage and persist only the download URL.
9. Debugging Tools are Basic Inside FlutterFlow
⚠
The Problem
FlutterFlow's Test Mode lets you check basic UI and navigation. But you
cannot set breakpoints, inspect the widget tree in real
time, or profile performance the way you can in a full Flutter IDE.
✔
The Fix
Use print() statements inside Custom Actions. Their output
appears in FlutterFlow's Run Log.
For hard bugs, download the code and use
Flutter DevTools in VS Code.
Quick Debug Inside FlutterFlow
Future<String> myAction(String input) async {
print('[myAction] called with: $input'); // appears in Run Log
try {
final result = await doWork(input);
print('[myAction] result: $result');
return result;
} catch (e) {
print('[myAction] ERROR: $e');
return 'error:$e';
}
}
Run Test Mode and trigger the action.
Look for the Run Log / Console panel — your print() lines appear there.
Full Debugging via Flutter DevTools
Download your project code from FlutterFlow (Export Code button).
Open it in VS Code and run: flutter run
Press 'v' in the terminal to open Flutter DevTools in your browser.
Use the Widget Inspector for layout issues, the Debugger for breakpoints, and the Performance tab for frame analysis.
Quick Reference
All 9 limitations and their solutions at a glance.
Limitation
Level
Solution
Firebase Rules & Pre-Auth Functions
Hard
Firebase Console for rules; secret key for pre-auth calls
API Call Timeout (~10 s)
Medium
Custom Action with .functions.invoke() and manual timeout
Custom Action in Dart (supports loops, try/catch, reuse)
State Management in Large Apps
Hard
Use the right state level; enable Real-Time on queries
No Try/Catch in Action Flow
Medium
Custom Action with try/catch + error string pattern
Limited Version Control
Easy
GitHub Sync + FlutterFlow Branches
No Binary Data in Persisted State
Medium
Base64 encode → store string → decode on load
Basic Debugging Tools
Medium
print() in Run Log → Flutter DevTools on download
Conclusion
FlutterFlow handles the majority of your app beautifully through its visual builder. The limitations in this guide are not dead ends — every single one has a tested workaround. The pattern is always the same:
Use the visual builder for everything it supports — it is fast and easy to maintain
Use Custom Actions for logic, loops, error handling, and long-running calls
Use Configuration Files inside FlutterFlow for native Android and iOS settings
Download the code only when you need Flutter DevTools or an extreme edge case
The more you practise these techniques, the more natural they become. You will develop an instinct for when to stay visual and when to drop into code.
Got an Idea? Let’s Validate It Before You Build It.
Not sure if your startup idea is ready to build? Talk to our experts about validating your idea, defining the right MVP, and figuring out what to test before investing months of time and money.