Skip to main content

Firestore security rules

About 3 min

Firestore security rules

In the previous chapters we built Travel Notes step by step:

  • Firebase setup created the Firebase project and connected Flutter.
  • Authentication made sure users can create an account and log in.
  • The Firestore ListView chapter stored trips under users/{uid}/trips.
  • Forms and validation improved the input screens before data is sent to Firebase.
  • An exercise added a numeric budget field to every trip.

Now we need to protect the data in Firestore itself. In the Firestore setup chapter we used test mode. That is useful while learning, but test mode is not safe for real apps. Firestore security rules decide which authenticated users may read or write which documents.

In Travel Notes, trips are personal. A logged-in user should only be able to read and write their own trips.

Why rules matter

Your Flutter app runs on the user's device. That means users can inspect the app, change requests or call Firestore directly. You should never rely on UI code to protect data.

Firestore security rules are checked by Firebase before client apps can read or write Firestore documents. They are not a replacement for good app code, but they are the server-side access checks for Firestore client requests.

Where to add rules

Add the rules in the Firebase console:

  • Open the Firebase consoleopen in new window.
  • Open your Travel Notes project.
  • Go to Databases & Storage > Firestore.
  • Open the Rules tab.
  • Replace the existing test mode rules with our upcoming rules.
  • Click Publish.

After publishing, run the app again and test with at least two accounts. Each account should only see its own trips.

A first safer rule

This rule only allows signed-in users to read and write.

rules_version = '2';

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

This is better than test mode, but it still means every signed-in user can read every document. For Travel Notes, that is not good enough.

Travel Notes data path

Our app stores data like this:

users
  userUid
    trips
      tripId
        title: "Summer in Japan"
        destination: "Tokyo"
        tags: ["city", "food", "culture"]
        checklist: ["Passport", "JR Pass", "Camera"]

The document id under users is the Firebase Authentication uid. This lets us write rules that only allow a user to access their own data.

The TripRepository from the Firestore ListView chapter uses the same path:

final uid = FirebaseAuth.instance.currentUser!.uid;

return FirebaseFirestore.instance
    .collection('users')
    .doc(uid)
    .collection('trips');

The rules must match this structure. If the app reads from users/{uid}/trips, then the rules should protect users/{uid}/trips.

Restrict trips to their owner

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null
        && request.auth.uid == userId;

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

With these rules, a user can only access documents under their own Firebase Authentication uid. For example, user abc123 can read and write under users/abc123/trips, but not under users/xyz789/trips.

Validate fields

Rules can also check the data that is written.

For the current Travel Notes app, replace the rules in the Firebase console with this safer version:

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    function isSignedInUser(userId) {
      return request.auth != null
        && request.auth.uid == userId;
    }

    function isTrip() {
      return request.resource.data.keys().hasOnly([
        'title',
        'destination',
        'description',
        'budget',
        'tags',
        'checklist',
        'createdAt',
      ])
      && request.resource.data.title is string
      && request.resource.data.destination is string
      && request.resource.data.description is string
      && request.resource.data.budget is number 
      && request.resource.data.budget >= 0
      && request.resource.data.tags is list
      && request.resource.data.checklist is list;
    }

    match /users/{userId}/trips/{tripId} {
      allow read: if isSignedInUser(userId);

      allow create, update: if isSignedInUser(userId)
        && isTrip();

      allow delete: if isSignedInUser(userId);
    }
  }
}

These are the rules that fit the current app:

  • isSignedInUser(userId) checks that the user is logged in and that the path contains their own uid.
  • match /users/{userId}/trips/{tripId} protects exactly the path used by TripRepository.
  • allow read lets users load their own trips in the TripListPage.
  • allow create, update lets users save trips only when the fields match the expected Travel Notes structure.
  • request.resource.data.budget is number checks that the budget contains a numeric value.
  • request.resource.data.budget >= 0 prevents users from storing a negative budget.
  • allow delete lets users delete only their own trips.

Client-side validation from the forms chapter helps users enter good data, but these rules are still needed. A user can skip the Flutter UI and send requests directly, so Firestore must check the path and fields too.

Rules are not filters

Firestore rules do not filter unsafe data for you. A query must only ask for data the user is allowed to read.

For example, if your rules only allow access to /users/{uid}/trips, then query that exact path in Flutter:

final uid = FirebaseAuth.instance.currentUser!.uid;

FirebaseFirestore.instance
    .collection('users')
    .doc(uid)
    .collection('trips');

This matches the TripRepository from the Firestore ListView chapter.

Exercise

Update the rules after adding the activities field in the previous exercise:

  • allow the activities field
  • check that activities is a list
  • keep every trip under users/{uid}/trips