# What your Firestore rules read decides your write order ([日本語](firestore-rules-write-order.md)) Do you have rules that look like this? ``` function isMember(groupId) { return exists(/databases/$(database)/documents/groups/$(groupId)/members/$(request.auth.uid)); } match /groups/{groupId} { allow read: if isMember(groupId); } ``` If so, **`members/{uid}` is no longer just another document.** It has become **a precondition for every read and write that follows**, which means **you can no longer write it in parallel with anything else.** The only error you get is `PERMISSION_DENIED`, so you end up rereading your rules looking for the mistake. **The rules are correct. Only the order is wrong.** --- ## Joining — establish the precondition first Say joining a group writes to several documents. **They're different documents, so parallelising looks safe.** ```dart // Fails intermittently await Future.wait([ writeMembership(), // ← the one the rules read writeProfile(), writeSomethingElse(), ]); ``` `Future.wait` **makes no promise about which write lands first.** If the group read runs before the membership document arrives, **the rules see a non-member** and reject it. ```dart // Settle the precondition first await writeMembership(); await Future.wait([writeProfile(), writeSomethingElse()]); ``` ### The symptom is misleading **Restarting the app fixes it** — by then the write has landed. If you see "the spinner never stops, but relaunching shows I've joined", the failure is **not the write. It's the read immediately after it.** --- ## Deleting — keep the precondition until last **The same rule governs deletion.** Removing a record also requires being a member. ```dart // Leaves orphans behind await Future.wait([ deleteMembership(), // ← if this lands first, you lose permission here deleteRecords(), deleteHalls(), ]); ``` Once the membership document is gone, the remaining deletes fail with `PERMISSION_DENIED` — and **you can never delete them, because you are no longer a member.** ```dart // Drop the precondition last await Future.wait([deleteRecords(), deleteHalls()]); await deleteMembership(); ``` ### It looks deleted on screen The list empties because **your own reference is gone.** The data is still there. **You find out on another device, or when you rejoin.** --- ## How to spot it **Search your rules file for `exists(` and `get(`.** ``` grep -nE "exists\(|get\(" firestore.rules ``` **Every path that appears there is a document whose write order matters.** Referencing a document inside a rule is a declaration that **"whether this document exists changes what else is allowed."** The dependency lives in the rules, **so reading your application code alone will never reveal it.** --- ## The principle **Don't decide what can be parallelised by asking whether the documents differ.** In Firestore, **documents are not peers.** Whatever the rules read is a precondition for the rest. | Operation | The precondition document | | --- | --- | | Create / join | **First, on its own** | | Normal reads and writes | Parallel is fine | | Delete / leave | **Last, on its own** | First when creating, last when deleting — both are the same shape: **keep the precondition true for longer than anything that depends on it.** --- ## Symptom to cause | Symptom | Where to look | | --- | --- | | Never finishes, but works after a restart | A read is running before the write lands | | `PERMISSION_DENIED` although the rules are right | The document the rules read isn't there yet | | Deleted, but still present on another device | Permission was dropped first; the rest couldn't be deleted | **None of the three look like a misconfiguration.** The rules are right, the permissions are right — **only the order is wrong.** --- Recorded 2026-08. Flutter / Cloud Firestore.