The ranking screen in one of my apps once got stuck on “Loading”. At the time, I concluded that the Firebase Realtime Database rules were missing .indexOn, added it, deployed the rules, and the problem went away.
While writing this article, I re-tested it on the emulator. The short answer: without .indexOn, get() and the REST API failed, but onValue still received the data. The error was Index not defined. Permission denied only appeared when the read rule was missing.
This article covers the results under each rule setup, what my original conclusion did not actually verify, and how to tell the two errors apart.
Sponsored
What happens to an orderByChild query without .indexOn?
get() and the REST API failed with Index not defined, while onValue returned the data.
I loaded ranking-style data into the Realtime Database in Firebase Local Emulator Suite (firebase-tools 15.16.0). From Firebase JS SDK 12.19.0, I sent a query with orderByChild('balance') and limitToLast(2).
| Rules | get() | onValue | REST API |
|---|---|---|---|
| No .indexOn, read allowed | Failed (Index not defined) | Received data | 400 (Index not defined) |
| .indexOn set, read allowed | Received data | (not tested) | 200 |
| .indexOn set, no read rule | Failed (Permission denied) | Failed (PERMISSION_DENIED) | 401 (Permission denied) |
| No .indexOn, no read rule | Failed (Permission denied) | (not tested) | 401 (Permission denied) |
Without .indexOn, get() failed with the following message, which even tells you which rule to add:
Error: Index not defined, add ".indexOn": "balance", for path "/scores", to the rules
Without a read rule, onValue failed with:
permission_denied at /scores: Client doesn't have permission to access the desired data.
When the read rule was missing, Permission denied came back first, whether or not .indexOn existed. The two errors have different causes and completely different messages.
What code did I use to compare them?
I passed the same query to get() and to onValue.
import { initializeApp } from 'firebase/app';
import { getDatabase, connectDatabaseEmulator, ref, get, onValue, query, orderByChild, limitToLast } from 'firebase/database';
const app = initializeApp({ projectId: 'demo-rinblog', databaseURL: 'https://demo-rinblog-default-rtdb.firebaseio.com' });
const db = getDatabase(app);
connectDatabaseEmulator(db, '127.0.0.1', 9000);
const q = query(ref(db, 'scores'), orderByChild('balance'), limitToLast(2));
try {
console.log('get()', (await get(q)).val());
} catch (e) {
console.log('get() error:', e.message);
}
onValue(q,
(snapshot) => console.log('onValue', snapshot.val()),
(e) => console.log('onValue error:', e.message));
Without .indexOn, only get() failed with Index not defined. onValue received the top two entries, with balances of 1200 and 800.
How does this fit with the official documentation?
The documentation says the realtime client libraries can run ad-hoc queries without indexes. In my test, that held for onValue, but get() failed just like the REST API.
Firebase’s Index Your Data guide says indexes are not required during development unless you use the REST API. It also says performance degrades as data grows, so you should add indexes before launch.
I did not verify why get() behaves like the REST API. What I can say from these results is that a query from an SDK does not automatically work without an index.
How should I read my earlier “it looked like permission-denied” note?
In this test, a missing .indexOn never produced Permission denied. Explaining the original symptom only by the missing .indexOn was a conclusion I had not fully verified.
The app fetched the ranking with orderByChild('balance').limitToLast(30).get(). Because it used get(), failing without .indexOn matches today’s results.
However, my notes from that time said the error “appeared on the client as permission-denied”. In this test, the error for a missing index was Index not defined, which is a different error from Permission denied.
▼What my original conclusion did not verify
▶ The fix was “deploying a rules file that added .indexOn“, so I never separated the whole rules file taking effect from the index being added
▶ I could not reproduce how the permission-denied error appeared in the Flutter app in this test
▶ The REST API’s Index not defined always appears without an index, so it is weak evidence for pinning down a single cause
I have no record of what rules were live in production before that deployment, so I cannot check it now. Given that the app used get(), I still think .indexOn was needed. But I can no longer say it was the only cause.
Treating the visible error message as the cause is the same trap I described in AI Reported a Failed Build as Success: Pipe Exit Codes Explained.
Sponsored
How do you tell permission denied apart from Index not defined?
Send the same query through the REST API to read the exact error, then reproduce it on the emulator by changing one rule at a time.
▼Steps to isolate the cause
① Send the same query to the REST API. 400 Index not defined means an index problem, 401 Permission denied means a rules problem
② Try both get() and onValue in the app. If only onValue works, suspect the index
③ Change rules one at a time on the emulator and see whether the same error comes back
④ After fixing, confirm which single change fixed it by changing only one thing at a time
How do you run these checks on the emulator?
If the project ID starts with demo-, you can start the emulator without logging in to Firebase.
The emulator needs Java. I used JDK 21.
firebase emulators:start --only database --project demo-rinblog
While it runs, you can replace the rules through the REST API. That lets you switch conditions without reloading the data.
# Replace the rules (no .indexOn, read allowed)
curl -X PUT "http://127.0.0.1:9000/.settings/rules.json?ns=demo-rinblog-default-rtdb" \
-H "Authorization: Bearer owner" \
-d '{"rules":{".read":true,".write":true}}'
# Send the equivalent of orderByChild through the REST API
curl "http://127.0.0.1:9000/scores.json?ns=demo-rinblog-default-rtdb&orderBy=%22balance%22&limitToLast=2"
Without .indexOn, the REST API returned 400 with this body:
{
"error" : "Index not defined, add \".indexOn\": \"balance\", for path \"/scores\", to the rules"
}
Where should .indexOn go in the rules?
Put it at the node you query, which is the ref you call orderByChild on. For per-week nodes, put it under the wildcard.
{
"rules": {
"scores": {
".read": "auth != null",
".indexOn": ["balance"]
},
"weekly": {
"$weekId": {
".read": "auth != null",
".indexOn": ["balance"]
}
}
}
}
I checked the case where you query a per-week node such as weekly/2026-W37 on the emulator.
| Where .indexOn is | get() on weekly/2026-W37 |
|---|---|
| Not set | Failed (Index not defined) |
Under weekly/$weekId |
Received data |
Directly under weekly (one level off) |
Failed (Index not defined) |
An index placed one level off does not work. Compare the path after for path in the error message with where you wrote the rule.
Sponsored
Related articles
▶ Why Firebase Realtime Database Returns a List for Numeric Keys…another Realtime Database behavior I verified on the same emulator
▶ AI Reported a Failed Build as Success: Pipe Exit Codes Explained…another case where a visible result led me to the wrong cause
Summary
▼Key points
① Without .indexOn, get() and the REST API fail with Index not defined, while onValue still returned data
② Permission denied comes from a missing read rule, not from a missing index
③ To isolate the cause, check the REST API’s error message and try both get() and onValue
④ Put .indexOn at the node you query. One level off does not work
Back then, I decided the cause was .indexOn because the problem went away. I did not notice that the fix bundled more than one change until I re-tested it. My current conclusion is that .indexOn was needed, but the permission-denied explanation was never verified.
Tested with Firebase Local Emulator Suite (firebase-tools 15.16.0, JDK 21) and Firebase JS SDK 12.19.0 on September 12, 2026. I did not verify the production Realtime Database or how Flutter’s firebase_database package displays these errors.