# How to Improve Your FlutterFlow App's Security

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45-60 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

Securing a FlutterFlow app means: replacing default Firestore test rules with strict per-user rules, moving all API keys to Cloud Functions so they never reach the client, enabling Firebase App Check to block fake clients, validating all user inputs server-side, and adding rate limiting on sensitive endpoints. Never deploy with the default 'allow read, write: if true' rules.

## A Practical Security Checklist for FlutterFlow Apps

Most FlutterFlow apps launch with significant security holes — not because builders are careless, but because FlutterFlow's default Firebase setup prioritizes getting something working over locking it down. The number one threat is the default Firestore test rules that allow anyone on the internet to read and write your entire database. This tutorial covers the full security checklist: Firestore rules, API key management, Firebase App Check, input validation, and rate limiting. Each step is actionable in under 10 minutes and dramatically improves your app's security posture.

## Before you start

- FlutterFlow project with Firebase connected
- Firebase console access for your project
- Firebase Authentication enabled with at least one sign-in method
- Basic understanding of Firestore collections and documents

## Step-by-step guide

### 1. Replace default Firestore test rules immediately

This is the most critical security fix. In the Firebase console, click Firestore Database in the left sidebar, then click the Rules tab. If you see 'allow read, write: if true;' — your database is completely public. Anyone with your project ID can read, write, and delete all your data. Replace the rules immediately with the template below that restricts all operations to authenticated users accessing only their own data. For collections that need different permissions (like public content or admin-only data), add specific rules above the default deny-all at the bottom.

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Users can only read and write their own profile
    match /users/{userId} {
      allow read, write: if request.auth != null
        && request.auth.uid == userId;
    }

    // Users can read public articles; only authors can write
    match /articles/{articleId} {
      allow read: if true;
      allow create: if request.auth != null;
      allow update, delete: if request.auth != null
        && request.auth.uid == resource.data.author_id;
    }

    // Orders: users see only their own; admins see all
    match /orders/{orderId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.user_id;
      allow read, write: if request.auth != null
        && request.auth.token.admin == true;
    }

    // Default deny-all for everything else
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
```

**Expected result:** Unauthenticated requests to your Firestore database return permission-denied errors. Authenticated users can only access documents they are authorized for.

### 2. Move all API keys to Cloud Functions — never store them in FlutterFlow

FlutterFlow compiles your app into JavaScript (for web) and Dart (for mobile). Any API keys you store in App State variables, Custom Actions, or widget properties are visible in the compiled app bundle — any user can extract them with basic tools. Instead, create a Firebase Cloud Function for each third-party API call (Stripe, SendGrid, OpenAI, etc.). Store API keys as Firebase environment secrets using 'firebase functions:secrets:set MY_API_KEY'. The Cloud Function reads the secret at runtime, makes the API call, and returns only the needed data to the app. In FlutterFlow, call the function using a Custom Action that hits the Cloud Function's HTTPS endpoint.

**Expected result:** Your app bundle contains zero API keys. All sensitive API calls are proxied through Cloud Functions that read secrets from Firebase's secure environment.

### 3. Enable Firebase App Check to block fake clients

Firebase App Check verifies that requests to your Firebase services come from your legitimate app and not from curl, Postman, or a competitor's script. In the Firebase console, go to App Check under the Build section. Click 'Get started', select your app (iOS and Android separately), and choose the attestation provider: Play Integrity for Android and App Attest for iOS (both free). On the Web, use reCAPTCHA Enterprise. Once enabled and enforced, only verified app instances can call Firestore, Storage, and Cloud Functions. Enforcement takes effect 7 days after enabling — use the debug token feature during development to test without affecting your real usage metrics.

**Expected result:** The App Check dashboard shows verified attestation requests from your app. Requests from curl or unauthorized clients receive a 403 App Check token is invalid response.

### 4. Validate and sanitize all user inputs before writing to Firestore

FlutterFlow's form validation (required fields, email format) only runs on the client. A determined attacker can bypass the app entirely and send malformed data directly to Firestore. Add server-side validation in two places: first, in your Firestore security rules using request.resource.data conditions to enforce field types and lengths; second, in Cloud Functions that handle sensitive writes. Check string lengths (request.resource.data.username.size() < 50), reject special characters in fields that should not have them, and verify numeric fields are within acceptable ranges. Never trust that data stored in Firestore was written by your app.

```
// Firestore rule example: validate article fields on create
match /articles/{articleId} {
  allow create: if request.auth != null
    && request.resource.data.title is string
    && request.resource.data.title.size() > 0
    && request.resource.data.title.size() <= 200
    && request.resource.data.body is string
    && request.resource.data.body.size() <= 50000
    && request.resource.data.author_id == request.auth.uid
    && request.resource.data.keys().hasOnly(
         ['title', 'body', 'tags', 'author_id', 'created_at']);
}
```

**Expected result:** Firestore rejects documents that fail validation — wrong field types, oversized strings, or unexpected extra fields — even when submitted directly without going through your app.

### 5. Add rate limiting on sensitive Cloud Functions

Without rate limiting, any authenticated user can hammer your Cloud Functions thousands of times — running up your Firebase bill or spamming other users. Implement rate limiting using Firestore to track request counts per user. In your Cloud Function, before processing the request, check a rate_limits/{userId} document. If the request count in the last minute exceeds your threshold, return a rate limit error immediately. Use FieldValue.increment to update the counter atomically and set a Firestore TTL to auto-delete old rate limit documents. For RapidDev clients building payment apps, we recommend 10 requests per minute for financial Cloud Functions and 60 per minute for general data operations.

```
// Cloud Function rate limiter middleware
const admin = require('firebase-admin');

async function checkRateLimit(userId, actionName, maxPerMinute = 20) {
  const db = admin.firestore();
  const key = `${userId}_${actionName}`;
  const ref = db.collection('rate_limits').doc(key);

  const now = Date.now();
  const windowStart = now - 60 * 1000;

  const result = await db.runTransaction(async (tx) => {
    const doc = await tx.get(ref);
    const data = doc.exists ? doc.data() : { count: 0, windowStart: now };

    // Reset window if older than 1 minute
    if (data.windowStart < windowStart) {
      tx.set(ref, { count: 1, windowStart: now, expiresAt: new Date(now + 120000) });
      return { allowed: true };
    }

    if (data.count >= maxPerMinute) {
      return { allowed: false };
    }

    tx.update(ref, { count: admin.firestore.FieldValue.increment(1) });
    return { allowed: true };
  });

  return result.allowed;
}
```

**Expected result:** Calling a rate-limited Cloud Function more than 20 times per minute returns a resource-exhausted error. Normal users are never affected because they naturally stay within the limit.

## Complete code example

File: `secure_firestore_rules_template.rules`

```text
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Helper functions
    function isSignedIn() {
      return request.auth != null;
    }
    function isOwner(userId) {
      return isSignedIn() && request.auth.uid == userId;
    }
    function isAdmin() {
      return isSignedIn() && request.auth.token.admin == true;
    }
    function validString(field, maxLen) {
      return field is string && field.size() > 0 && field.size() <= maxLen;
    }

    // User profiles
    match /users/{userId} {
      allow read: if isSignedIn();
      allow create: if isOwner(userId)
        && validString(request.resource.data.display_name, 100)
        && request.resource.data.keys().hasOnly(
             ['display_name', 'email', 'created_at', 'avatar_url']);
      allow update: if isOwner(userId)
        && validString(request.resource.data.display_name, 100);
      allow delete: if isOwner(userId) || isAdmin();
    }

    // Public content — readable by all, writable by author
    match /articles/{articleId} {
      allow read: if true;
      allow create: if isSignedIn()
        && validString(request.resource.data.title, 200)
        && request.resource.data.author_id == request.auth.uid
        && request.resource.data.keys().hasOnly(
             ['title', 'body', 'tags', 'author_id', 'created_at', 'status']);
      allow update: if isOwner(resource.data.author_id) || isAdmin();
      allow delete: if isOwner(resource.data.author_id) || isAdmin();
    }

    // Orders — private to owner
    match /orders/{orderId} {
      allow read, write: if isOwner(resource.data.user_id);
      allow read, write: if isAdmin();
    }

    // Admin-only collections
    match /admin_settings/{docId} {
      allow read, write: if isAdmin();
    }

    // Default deny-all
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
```

## Common mistakes

- **Deploying with the default Firestore test rules (allow read, write: if true)** — Anyone on the internet who discovers your Firebase project ID can read, modify, and delete your entire database. Firebase project IDs are often visible in JavaScript bundles and app metadata. Fix: Replace test rules immediately with authentication-based rules. Go to Firebase console → Firestore → Rules and deploy the secure template from this tutorial before your first real user signs up.
- **Storing third-party API keys in FlutterFlow App State or Custom Action code** — FlutterFlow compiles to Dart for mobile and JavaScript for web. Both formats can be decompiled or inspected. Any key stored in the app is a public key. Fix: Store all API keys as Firebase Function secrets and proxy every third-party API call through a Cloud Function. The function returns only the data the app needs, never the raw API key.
- **Relying only on client-side form validation for security** — FlutterFlow form validators run only in the app. Attackers bypass the app and send API calls directly to Firestore with any payload they want. Fix: Add field validation to your Firestore security rules using request.resource.data type checks, size limits, and the hasOnly() keys restriction.
- **Enabling App Check enforcement immediately without a monitoring period** — App Check occasionally blocks legitimate users on older app versions or in regions with strict network policies. Enforcing on day one can lock out real users. Fix: Enable App Check in monitoring mode first. Review the App Check metrics dashboard for 7 days to confirm no legitimate requests are being blocked, then click Enforce.

## Best practices

- Never deploy with default Firestore test rules. Make rule deployment part of your pre-launch checklist.
- Use Firebase's Blaze plan and set a spending alert at $5 and $20 — unexpected cost spikes are often the first sign of unauthorized API usage.
- Enable Firebase Authentication email enumeration protection so attackers cannot discover which email addresses are registered.
- Use the hasOnly() rule check to prevent users from injecting unexpected fields like isAdmin into their own documents.
- Rotate API keys quarterly and revoke old keys immediately after any suspected breach.
- Enable Firestore audit logging (Cloud Audit Logs) in production to maintain a record of all data access for compliance.
- Test your Firestore rules using the Firebase emulator suite locally before deploying — a rules mistake can lock your own app out of its data.
- Set up Firebase Security Rules unit tests using the @firebase/rules-unit-testing package so rule changes are caught before deployment.

## Frequently asked questions

### How do I know if my Firestore rules are currently open to the public?

Open the Firebase console, go to Firestore Database, and click the Rules tab. If you see 'allow read, write: if true;' anywhere without an authentication condition, your database is publicly accessible. Fix this immediately.

### Can someone extract my API keys from my FlutterFlow app?

Yes. FlutterFlow compiles to Dart (mobile) and JavaScript (web). Both can be decompiled or inspected in browser developer tools. Any API key stored in the app code, App State, or widget properties is visible to anyone who looks. Always proxy sensitive API calls through Firebase Cloud Functions.

### What is Firebase App Check and do I need it?

App Check verifies that requests to Firebase come from your actual app, not from scripts or unauthorized clients. It is not mandatory but strongly recommended for any production app. Without it, anyone who finds your Firebase config can call your Cloud Functions and query Firestore directly.

### Does FlutterFlow's form validation protect my database?

No. Form validation in FlutterFlow only runs in the app UI. An attacker can skip the app entirely and send API requests directly to Firestore. You must add validation in your Firestore security rules using request.resource.data checks.

### How do I set an admin role for specific users in Firebase?

Use Firebase Admin SDK in a Cloud Function to set a custom claim: auth.setCustomUserClaims(uid, { admin: true }). The user must sign out and back in for the claim to appear in their ID token. Reference it in Firestore rules with request.auth.token.admin == true.

### Will stricter Firestore rules break my FlutterFlow app?

They might if your app queries data without passing the user's authentication token. Test rule changes in the Firebase Rules Playground first and use the FlutterFlow preview mode to verify all screens still load correctly after tightening rules.

### How much does Firebase App Check cost?

Firebase App Check is free. Play Integrity (Android) and App Attest (iOS) attestation providers have no direct cost from Firebase. reCAPTCHA Enterprise for web has a free tier of 10,000 assessments per month.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-can-i-improve-my-flutterflow-app-s-security
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-can-i-improve-my-flutterflow-app-s-security
