Firestore security rules
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
budgetfield 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 console.
- 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 ownuid.match /users/{userId}/trips/{tripId}protects exactly the path used byTripRepository.allow readlets users load their own trips in theTripListPage.allow create, updatelets users save trips only when the fields match the expected Travel Notes structure.request.resource.data.budget is numberchecks that the budget contains a numeric value.request.resource.data.budget >= 0prevents users from storing a negative budget.allow deletelets 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
activitiesfield - check that
activitiesis a list - keep every trip under
users/{uid}/trips