Insights 8 min read

Firebase Security Rules for FlutterFlow: A Practical Guide

Share this insight

Key Takeaway

FlutterFlow connects to Firestore in test mode by default, which means anyone with your app's config can read and write your entire database. The fix is to write real security rules: require authentication with request.auth, enforce per-user ownership so people only touch their own documents, gate sensitive actions behind roles, and validate the shape of every write. Test mode is not a starting point you can forget about. It is a public door you have to close before launch.

If you built an app in FlutterFlow and connected it to Firebase, there is a good chance your database is currently wide open to the entire internet. Not hacked, not misconfigured by accident, just left in the state FlutterFlow set up for you to get moving quickly. This guide walks through exactly how Firestore security rules work, the specific traps FlutterFlow apps fall into, and copy-ready rule snippets you can adapt. The goal is a database where every read and write is checked, not assumed.

Security rules are not a Firebase feature you bolt on later. They are the only thing standing between your users' data and anyone who opens your app and pokes at the network traffic. FlutterFlow is a client. The rules live on Firebase's servers, and they are the real authority.

The default test-mode trap

When you create a Firestore database, Firebase asks whether you want to start in test mode or production mode. FlutterFlow's setup flow, and most tutorials, point you at test mode because it lets your app read and write immediately with no friction. Here is what test mode actually installs:

The test-mode default looks like this:

  • allow read, write: if request.time < timestamp.date(2026, 1, 1);

That single line says: allow anyone to read and write any document until a hard-coded date. There is no check on who the user is. The date is the only gate, and once it passes, Firebase flips to denying everything, which is its own kind of outage. Before that date, your entire database is public. Anyone who extracts your Firebase project config (which ships inside every published app and is trivial to read) can query every collection, dump every user's records, and overwrite or delete data at will.

This is the single most common way FlutterFlow apps leak data. The app looks finished, the screens work, the data loads, and nothing on the surface tells you the back door is open. The fix is to replace that placeholder with rules that check identity and ownership on every operation.

Test mode vs. production rules at a glance

Before the snippets, here is the difference in plain terms. Test mode is a temporary scaffold. Production rules are the actual lock.

ConcernTest mode (default)Production rules
Who can readAnyone, until a fixed dateOnly authenticated, authorized users
Who can writeAnyone, until a fixed dateOnly the document owner or an admin
Write validationNoneRequired fields, types, and limits enforced
What happens at the dateAll access silently breaksNo expiry, rules apply indefinitely
Safe to launch?No, neverYes, once tested

How Firestore security rules actually work

Security rules live in the Firebase console under Firestore Database, then the Rules tab. They are written in a small, purpose-built language, not Dart and not JavaScript. Every request from your FlutterFlow app is evaluated against them before Firebase does anything. A rule either allows the operation or denies it. Deny is the default, so if no rule grants access, the request fails.

The basic shape is a match block that targets a path, and inside it an allow statement for an operation:

  • match /collectionName/{docId} { allow read, write: if <condition> }

The {docId} part is a wildcard that captures the document ID so you can reference it. The condition is a boolean expression. The operations you can gate are read, write, and the finer-grained get, list, create, update, and delete. One important and often-missed rule of the language: security rules do not cascade into subcollections automatically. A rule on a parent document does not cover documents nested underneath it. You have to write a match for each level you want to protect.

Read and write are shorthand. The granular operations are the real control.

It helps to know that read and write are just umbrellas over more specific operations. Reaching for the granular ones lets you allow a person to create a record but never delete it, or list their own items without being able to read someone else's by guessing an ID. Here is how they map:

OperationCovered byWhat it controlsData object to check
getreadFetching a single document by IDresource.data
listreadRunning a query over a collectionresource.data
createwriteAdding a new documentrequest.resource.data
updatewriteChanging an existing documentboth, old and new
deletewriteRemoving a documentresource.data

Step one: require authentication with request.auth

The first real rule almost every app needs is simple: no anonymous access. The request.auth object is null when the caller is not signed in, and it carries the user's ID and token when they are. Checking it is the foundation everything else builds on.

Require any signed-in user:

  • match /databases/{database}/documents {
  •   match /{document=**} {
  •     allow read, write: if request.auth != null;
  •   }
  • }

This is a real improvement over test mode, and for a small internal tool it might even be enough. But be honest about what it does: it lets any logged-in user read and write any document. User A can read User B's private records. It closes the door to the public but leaves every room inside unlocked. For almost any real app, you need ownership rules on top of this.

One FlutterFlow-specific note: if you use anonymous authentication (a common pattern for letting people try the app before signing up), request.auth will not be null for those users either. Anonymous auth still produces a valid auth token. If you want to exclude anonymous users from sensitive actions, check request.auth.token.firebase.sign_in_provider != 'anonymous'.

Step two: per-user document ownership

Ownership rules tie a document to the user who is allowed to touch it. The standard pattern stores the owner's user ID on the document, usually in a field like uid or userId, and the rule compares that field against request.auth.uid.

There are two common layouts. The first is a collection where the document ID is the user's ID, which is typical for a users profile collection:

  • match /users/{userId} {
  •   allow read, write: if request.auth != null && request.auth.uid == userId;
  • }

The second is a collection of records (orders, posts, notes) where ownership is stored in a field. Here you check the document's data, and you have to handle reads and writes slightly differently because on a create there is no existing document yet:

  • match /notes/{noteId} {
  •   allow read, update, delete: if request.auth != null && resource.data.userId == request.auth.uid;
  •   allow create: if request.auth != null && request.resource.data.userId == request.auth.uid;
  • }

The distinction between resource and request.resource trips up almost everyone. resource.data is the document as it exists in the database right now (use it for read, update, delete). request.resource.data is the document as the client is trying to write it (use it for create and update). On a create there is no existing resource, so you must validate against request.resource instead, otherwise the rule errors and the write is denied.

Step three: role-based access

Plenty of apps need more than owner-or-not. An admin should see everything. A moderator should edit any post. The cleanest way to do roles in Firestore is to store the role somewhere the rules can read it. You have two solid options.

Option A, a role field on the user document. The rule fetches the caller's user doc and checks the field. This is easy to set up entirely inside FlutterFlow and Firestore:

  • function isAdmin() {
  •   return request.auth != null &&
  •     get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
  • }

Then reuse it: allow read, write: if isAdmin() || resource.data.userId == request.auth.uid;

The trade-off is that every get() inside a rule counts as a document read and adds a small amount of latency. For most apps that is fine. Just know it is not free.

Option B, custom claims on the auth token. You set a claim like admin: true on the user's auth token using the Firebase Admin SDK (from a Cloud Function or a trusted server), and the rule reads it directly with no extra document fetch: request.auth.token.admin == true. This is faster and the more correct approach for security-sensitive roles, because a regular user cannot edit their own auth token, whereas a role field in Firestore could in theory be changed if your write rules are loose. The catch is that setting custom claims requires server-side code, which is a step beyond what FlutterFlow gives you out of the box.

Step four: validating writes

Authentication and ownership decide who can write. Validation decides what they can write. Without it, a user who is allowed to create their own document can still stuff it with garbage: a negative balance, a missing required field, an absurdly long string, or a field that promotes them to admin. Validation rules check the shape and content of request.resource.data before allowing the write.

A create rule with real validation:

  • allow create: if request.auth != null
  •   && request.resource.data.userId == request.auth.uid
  •   && request.resource.data.keys().hasAll(['userId', 'title', 'createdAt'])
  •   && request.resource.data.title is string
  •   && request.resource.data.title.size() < 200
  •   && request.resource.data.createdAt == request.time;

A few patterns worth knowing. Use hasAll([...]) to require fields and hasOnly([...]) to forbid extra fields, which stops a user from sneaking in a role or isPremium field they should not control. Use is string, is int, is timestamp, and is bool to enforce types. On updates, you can compare old and new values, for example request.resource.data.balance == resource.data.balance to prevent a client from ever changing a field that should only be set by your backend. This is the layer that turns "the right user can write" into "the right user can only write the right thing."

Common mistakes that leave FlutterFlow data public

Most data exposure in FlutterFlow apps comes from a short list of repeat offenders:

  • Shipping with test-mode rules. The number-one cause. The placeholder rule never gets replaced, and the app launches with a public database.
  • Using allow read, write: if true; to make an error go away. If a rule is blocking something during development and the fix is "set it to true," you have just turned the lock off. This sticks around far more often than anyone admits.
  • Trusting the FlutterFlow client to enforce security. Hiding a button or filtering a list in the app does nothing. Anyone can call Firestore directly with your config. Rules are the only enforcement that matters.
  • Forgetting subcollections. A locked-down users collection with an open users/{id}/messages subcollection is still leaking. Rules do not cascade. Write a match for every level.
  • Mixing up resource and request.resource. This usually surfaces as writes mysteriously failing, but the inverse (a too-permissive rule) silently allows bad writes.
  • Leaving a wildcard match /{document=**} with a loose condition. A broad catch-all rule can quietly override the careful rules you wrote for specific collections.

Test your rules before you trust them

Firebase gives you a Rules Playground inside the console (on the Rules tab) where you can simulate a read or write as a specific user against a specific path and see whether it is allowed or denied. Use it. Run through the cases that matter: a signed-out user trying to read, User A trying to read User B's document, a user trying to create a document owned by someone else, a user trying to write a field they should not control. For larger projects, the Firebase Local Emulator Suite lets you write automated tests for your rules so a future change does not quietly reopen a door you closed. Rules are code. They deserve the same testing as the rest of your app.

When done-for-you makes more sense than DIY

Everything above is real and workable, and if you enjoy this kind of thing, you can absolutely secure a FlutterFlow app yourself. Here is the honest part. Security rules are never finished. Every new collection needs new rules. Every feature can introduce a new path that the catch-all does not cover. Roles, validation, and the resource versus request.resource distinction are exactly the kind of detail that is easy to get subtly wrong, and a subtle mistake here is the difference between a private database and a public one. Most of the data breaches you read about involving no-code and low-code apps trace back to misconfigured access rules, not clever attackers.

This is precisely the work an operator handles so you do not have to. Rehost is a done-for-you team based in Los Angeles. We design, build, host, monitor, and operate custom apps, and writing and maintaining the security layer is part of operating an app, not an add-on. You do not log into a Firebase console or learn the rules language. You send a plain message describing what should change, and we ship it, security rules included and kept current as the app grows. If you want the full picture, here is how the model works and the business page covering who it fits. You still own everything: the code repo, the developer accounts, the domain, and the customer data, all portable if you ever leave.

Pricing for business apps starts at $950/mo as of 2026, billed by monthly active users, with no setup fee and month-to-month terms. A full app build typically takes around two weeks. If you want to see how that maps to your situation, the app calculator gives you a quick estimate, and the numbers in detail live on the pricing page.

FAQ

Are FlutterFlow apps secure by default?

No. FlutterFlow connects to Firestore in test mode by default, which allows anyone to read and write your entire database until a hard-coded expiry date. The app itself can be well built, but the database stays public until you replace the test-mode rule with real security rules that check authentication and ownership. Security is configured in Firebase, not in FlutterFlow.

How do I write Firestore security rules for FlutterFlow?

You write them in the Firebase console under Firestore Database, then the Rules tab. Start by requiring authentication with request.auth != null, then add per-user ownership by comparing a userId field to request.auth.uid, then layer on role checks and write validation. The rules live on Firebase's servers and apply to every request from your app, no matter how the request is made.

What is the difference between resource and request.resource in security rules?

resource.data is the document as it currently exists in Firestore, used for read, update, and delete checks. request.resource.data is the document the client is trying to write, used for create and update checks. On a create there is no existing document, so you validate against request.resource. Mixing these up is one of the most common reasons rules either fail unexpectedly or allow writes they should not.

Do Firestore security rules cascade to subcollections?

No. A rule on a parent document does not automatically protect documents in its subcollections. You have to write a separate match block for each level of nesting you want to secure. A locked-down top-level collection with an unsecured subcollection underneath is a very common and easy-to-miss leak.

Can someone bypass my rules by editing the FlutterFlow app?

They do not even need to edit the app. Your Firebase project config ships inside every published app and is readable, so an attacker can query Firestore directly without ever opening your interface. Hiding buttons or filtering lists in FlutterFlow is cosmetic. Security rules on the Firebase server are the only enforcement that actually protects your data.

The bottom line

FlutterFlow makes it fast to connect an app to Firestore, and that speed comes with a default that leaves your database public. Closing that door is not optional and it is not a one-time task. Require authentication, enforce per-user ownership, gate sensitive actions behind roles, validate every write, and test the result before you trust it. Do that and you have a real, defensible data layer. If you would rather not own the ongoing work of keeping that layer correct as your app grows, that is exactly the kind of thing a done-for-you operator handles for you. Tell us what you are building and we will tell you honestly whether you are better off doing it yourself or having us run it.

Let us handle it.

Do-It-For-Me

Stop debugging platform limitations. Hand off your application to certified experts. We provide dedicated engineering, ongoing maintenance, and guaranteed SLAs starting at $950/month for business and startup applications. Transparent timelines, zero hidden fees.

Simple contract · Cancel anytime

Share this article

Build with us.

Turn insights into action. Let's build something great together.