We are Official Certified bubble.io & flutterflow  App Development partner
Check here
FlutterFlow Training
Sheharyar Ahmad
July 25, 2026
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

  1. Go to console.firebase.google.com and open your project.
  2. Click Firestore Database in the left sidebar, then select the Rules tab.
  3. 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;

    }

  }

}

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

Step 1 — Store the Secret in Firebase

  1. In your terminal, run:

firebase functions:config:set app.secret="YOUR_STRONG_SECRET_HERE"

firebase deploy --only functions

  1. In your Cloud Function, verify the key on every request:

const expected = functions.config().app.secret;

const received = req.headers['x-app-secret'];

if (received !== expected) { res.status(403).send('Forbidden'); return; }

// Safe to proceed

Step 2 — Store the Secret in FlutterFlow

  1. Go to FlutterFlow Settings > Environment Values.
  2. Add a variable named APP_SECRET. Mark it as sensitive.

Step 3 — Send the Key from a Custom Action

final response = await http.post(

  Uri.parse('https://YOUR-PROJECT.cloudfunctions.net/yourFunction'),

  headers: { 'x-app-secret': 'YOUR_SECRET', 'Content-Type': 'application/json' },

  body: jsonEncode({'email': email}),

).timeout(const Duration(seconds: 30));

Same pattern for Supabase Edge Functions

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:

Future<dynamic> saveAnswer(String questionId, String answerText) async {

  // 1. Refresh the session token before calling

  await SupaFlow.client.auth.refreshSession();

  if (SupaFlow.client.auth.currentSession == null) {

    return {'success': false, 'message': 'Not authenticated'};

  }

  // 2. Call the Edge Function by name — no URL or key needed

  try {

    final response = await SupaFlow.client.functions

        .invoke('your_function_name', body: {'question_id': questionId, 'answer': answerText})

        .timeout(const Duration(seconds: 30));  // set your timeout here

    final body = response.data as Map<String, dynamic>?;

    return body ?? {'success': false, 'message': 'Empty response'};

  } on TimeoutException {

    return {'success': false, 'message': 'Request timed out'};

  } catch (e) {

    return {'success': false, 'message': e.toString()};

  }

}

Timeout guidance

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

  1. Add a Page State variable: isLoading (Boolean, default: false).
  2. Add a CircularProgressIndicator. Set Conditional Visibility to: isLoading = true.
  3. 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

  1. In FlutterFlow, go to Settings (the < / > icon in the sidebar).
  2. Select the Project Dependencies tab.
  3. 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.

  1. Go to FlutterFlow Settings > Permissions.
  2. Toggle ON the permissions your package needs.
  3. 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:

  1. Go to Custom Code > Configuration Files.
  2. Select the file — e.g. AndroidManifest.xml or Info.plist.
  3. 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.

<!-- Example Snippet for AndroidManifest.xml -->

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<!-- Example Snippet for Info.plist -->

<key>NSMicrophoneUsageDescription</key>

<string>Used to record audio in the app.</string>

Files you can now edit inside FlutterFlow

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

  1. Go to Custom Code > Custom Actions > Add Action.
  2. Name it, set the return type, and add input parameters.
  3. Write your Dart logic. Short example — looping through a list:

Future<double> calcTotal(List<double> prices, double taxRate) async {

  double subtotal = 0;

  for (final p in prices) { subtotal += p; }   // loop — not possible visually

  return subtotal * (1 + taxRate);

}

  1. Click Compile, then Save.
  2. In the Action Flow Editor, add the Custom Action and map your parameters.
  3. 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

  1. Go to FlutterFlow Settings > Integrations > GitHub.
  2. Click Connect and authorise FlutterFlow to access your GitHub account.
  3. Select or create a repository.
  4. Click Push to GitHub. Each push creates a timestamped commit — your history is now on GitHub.
  5. 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

Future<String> encodeToBase64(Uint8List bytes) async {

  return base64Encode(bytes);

}

// Load: convert persisted Base64 string → bytes → use in audio player / image

Future<Uint8List> decodeFromBase64(String b64) async {

  return base64Decode(b64);

}

Real-World Example — Saving an Audio Recording

  1. In App State, add a variable: savedRecordingBase64 | Type: String | Persisted: ON
  2. After recording completes, call encodeToBase64 with the audio bytes.
  3. Save the returned string to savedRecordingBase64.
  4. On next app open, call decodeFromBase64 with savedRecordingBase64 to get the bytes back.
  5. 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';

  }

}

  1. Run Test Mode and trigger the action.
  2. Look for the Run Log / Console panel — your print() lines appear there.

Full Debugging via Flutter DevTools

  1. Download your project code from FlutterFlow (Export Code button).
  2. Open it in VS Code and run: flutter run
  3. Press 'v' in the terminal to open Flutter DevTools in your browser.
  4. 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
Package Native Configuration Easy Permissions panel → Config Files editor inside FlutterFlow
Complex Business Logic Easy 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.

FAQs

1. What are the main limitations of FlutterFlow?

FlutterFlow’s main limitations include complex business logic, advanced security rules, debugging limitations, state management challenges, version control restrictions, and limited support for storing binary data.

2. Can FlutterFlow build complex applications?

Yes, FlutterFlow can build complex applications. However, advanced features may require Custom Actions, Dart code, backend configuration, or native Flutter customization.

3. Can I add custom code in FlutterFlow?

Yes, FlutterFlow supports Custom Actions and Custom Functions, allowing developers to add Dart code for advanced workflows and logic.

4. Does FlutterFlow support Firebase Security Rules?

Yes, FlutterFlow works with Firebase Security Rules. Simple apps can use built-in controls, while advanced apps may require custom Firebase rules.

5. Can FlutterFlow handle long API requests?

FlutterFlow’s built-in API calls have timeout limitations. For longer processes like AI generation or complex backend tasks, Custom Actions can provide more control.

6. Can FlutterFlow store files or audio recordings in App State?

No, persisted App State does not directly support files or binary data. You need to convert the data or store files using services like Firebase Storage or Supabase Storage.

7. Does FlutterFlow support GitHub integration?

Yes, FlutterFlow supports GitHub Sync and FlutterFlow Branches, which help with version tracking and safer development workflows.

8. Is FlutterFlow suitable for production apps?

Yes, FlutterFlow can be used for production apps. The key is knowing when to use visual tools and when to extend functionality with custom code.
Incept MVP
Typically Replies within a day
Incept MVP
Hi there 👋
How can I help you?
Start Chat