Your Firebase read rule belongs on the node you listen to, not the node you read
Abbas
July 26, 2026
On Life Island, visitors can chat with each other. For a while, they could send messages that nobody β including the sender β would ever see.
The writes succeeded. The data was in the database; I could see it in the console. No client received it.
The rules, and the wrong instinct
The security rules read like this:
"chat": {
"island": {
"$entry": {
".read": "auth != null",
".write": "auth != null"
}
}
}
That looks correct. Each message is readable by any authenticated user. The instinct is to go hunting through the client code, the auth state, the listener setup β because surely the rule is fine.
What actually cracked it
The diagnostic was a comparison, not an inspection: presence worked perfectly. Same database, same auth, same client, same style of listener. Why would one live subtree work and the other not?
Because presence had its .read declared one level higher β on the room, not on each member.
That is the whole bug.
The rule that matters is the one on the node you attach to
Firebase Realtime Database rules do not cascade upward. When you call:
onChildAdded(ref(db, 'chat/island'), handler);
you are asking to read chat/island β the list itself. The database checks the rule at that path. A .read sitting on $entry grants permission to read an individual message if you request it directly by key, which is not what a child listener does.
So every message was readable in principle and the list containing them was readable by nobody.
The fix is to move one line up a level:
"chat": {
"island": {
".read": "auth != null",
"$entry": {
".write": "auth != null"
}
}
}
Rule of thumb: put .read on the path you subscribe to, and .write on the path you write to. They are often not the same path, and nothing in the API hints at the difference.
The second bug in the same commit
Once messages appeared, some of them immediately disappeared again β dropped by a replay filter meant to ignore stale history on join.
The filter compared the message's timestamp, stamped with the sender's Date.now(), against the receiver's clock. Two devices, two clocks, no relationship between them. A sender whose clock ran a few seconds slow produced messages that the receiver classified as old news and discarded.
Any time you compare a timestamp from one machine against Date.now() on another, you have written a bug that appears only on other people's devices. Either use a server timestamp as the single source of truth, or filter on something that isn't a clock at all β a session join marker, or the last key you've already seen.
What I took from it
When one feature works and its near-identical neighbour doesn't, stop reading the broken one. Diff the two. The difference is the bug, and it took a fraction of the time that staring at the chat code did.
Commit 984cd72 in Life Island.