Overview:
I am facing an issue with the real-time listeners in my React project, where it conflicts with security rules in Firestore. Specifically, security rules checks for listeners are triggered before a new document is created, leading to denied permissions.
It is a race condition with the following events:
- Firebase sdk initiates creation of new document
- App is instantly aware of new document through real time listener
- App creates a new listener for a subcollection of said document
- Firestore rules engine need to check a field of said document to grant access
- Since document doesn't exist yet in firestore backend, permissions are denied
- Event 1 finishes: document is created in firestore backend (too late)
Details:
In my project, I have a Firestore collection called adventures and a sub-collection called layers. I use the "firebase/firestore" library to create a new adventure in Firestore, which in turn triggers a real-time update of my listeners.
Here's how I create a new adventure in my React project:
import {addDoc, collection} from "firebase/firestore";
[...]
const adventuresRef = collection(firestore,"adventures")
const adventureId = (await addDoc(adventuresRef, newAdventure)).id;
Here is the listener for the user adventures:
import { useFirestoreCollectionData } from "reactfire";
[...]
const firestoreQuery = query(adventuresRef, where("userId", "==", userId), orderBy("createdAt"));
const { data: userAdventures } = useFirestoreCollectionData(firestoreQuery, {
idField: "id",
});
Additionally, I have listeners for listing the layers of each adventure.
The user adventures listener updates before the new adventure finishes being created in the backend (which is expected for real-time). However, my security rule for the layers compares the authenticated user ID against the adventure's userId and only allows reading if they match. Due to the real-time update, the security validation happening on the Firestore backend triggers before the adventure is created, causing the validation to fail.
Details of my security rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
[...]
function getAdventure(adventureId) {
return get(/databases/$(database)/documents/adventures/$(adventureId)).data
}
function getIsOwnerOrIsPublic(adventureId) {
let adventure = getAdventure(adventureId);
let isOwner = adventure.userId == request.auth.uid;
let isPublic = adventure.isPublic == true;
return isOwner || isPublic
}
function getCanRead(adventureId) {
return getIsOwnerOrIsPublic(adventureId) || [...]
}
[...]
match /adventures/{adventureId} {
[...]
match /layers/{layerId} {
allow read: if getCanRead(adventureId);
[...]
}
[...]
}
}
}
Current Workaround
As a temporary workaround, I create the adventure through a Node.js backend, not leveraging real-time updates. I am looking for suggestions on how to address this issue better.