The admin panel I use to manage a sales team collects leads from multiple sources. One of them is a Facebook campaign for distributing POS terminals, landing in a separate Firebase Realtime Database that until now was only visible through the Firebase Console web UI. The goal: surface those leads directly in the Admin, in a dedicated section for authorized users, sorted by submission date.
Adding a new tab to the existing sidebar dispatcher is a surgical operation — three touch points (tabs array, labels map, else if dispatcher) plus the actual render function. What should've been routine turned into three separate, unrelated bugs, each hiding the next.
Problem one: wrong DB node
The first version of the load function pointed at the database root:
// ❌ Points to root — returns null
const url = 'https://[MY-PROJECT]-default-rtdb.firebasedatabase.app/.json';
// ✅ Data lives under the /leads node
const url = 'https://[MY-PROJECT]-default-rtdb.firebasedatabase.app/leads.json';
The database had read rules open only on the leads node, not at the root. A fetch to /.json returned null because the root itself isn't accessible under proper rules. The fix was a three-character change to the URL.
Firebase RTDB rule: read rules are applied node by node. Having
".read": trueon/leadsdoes not grant access to the root. Always test the direct URL in the browser before writing any code.
Problem two: wrong mapping keys
Past the node problem, the table loaded but every column showed dashes (—). Records existed, but no field read correctly — because I'd guessed key names instead of verifying them.
The original mapping used "intuitive" names like tipo_pos, quantita, cliente — while the DB actually contained:
// Actual keys in the Firebase DB
{
"cognome": "Roversi",
"nome": "Andrea",
"email": "email@example.com",
"piano": "Smart",
"pos_quantita": 1,
"pos_tipo": "POS con Stampante",
"source": "facebook-lead",
"timestamp": "2026-06-09T11:29:19.980Z",
"tipo_cliente": "Privati & Startup"
}
Once the keys were corrected, everything read fine. The ISO 8601 timestamp gets formatted with toLocaleDateString('it-IT') before display.
Quick debug technique: if you can't fetch directly from your dev environment, just open the
.jsonURL in the browser. Firebase RTDB returns readable JSON with exact key names, no tooling required.
Problem three: the dynamic ID that cut records in half
With correct keys the table loaded — but showed only 2 records out of 4 in the DB. No console errors, no filters applied, all the data was there. This was the tricky one.
Root cause: setTimeout + Date.now()-based ID
renderSection() was generating a unique container ID on every call:
function renderSection() {
const containerId = 'section_' + Date.now(); // ← unique ID on every render
const html = `<div id="${containerId}">Loading...</div>`;
mainArea.innerHTML = html;
// Timeout fires AFTER the DOM is updated
setTimeout(() => _loadSection(containerId), 50);
}
When the user clicked the tab, renderSection() fired twice in quick succession — once from the click, once from the internal routing system syncing state. The second call overwrote the DOM with a new container ID before the first call's setTimeout fired. When that timeout ran, it looked for the old ID — gone, nothing written. The second call succeeded but caught the fetch mid-flight. Net result: anywhere between 2 and 4 records visible depending on device speed.
Fix: fixed ID + requestAnimationFrame
function renderSection() {
const containerId = '_sectionContainer'; // ← fixed, stable ID
const html = `<div id="${containerId}">Loading...</div>`;
mainArea.innerHTML = html;
// requestAnimationFrame guarantees the DOM is updated
// before looking up the container — more reliable than setTimeout
requestAnimationFrame(() => _loadSection(containerId));
}
With a fixed ID, any subsequent call always finds the same container, so the last render wins correctly. requestAnimationFrame runs the callback in the next browser frame, after layout is calculated and the node is guaranteed present — no arbitrary millisecond guess involved.
General rule:
setTimeout(fn, N)to wait for a DOM update is fragile — the rightNdepends on device speed.requestAnimationFrameis deterministic: it fires exactly after the browser finishes rendering the current frame.
Table layout without horizontal scroll
Once the loading bugs were fixed, the table overflowed horizontally. The culprit: white-space: nowrap on cells combined with generous padding preventing text wrap.
/* Fixed layout with percentage column widths */
table {
table-layout: fixed;
width: 100%;
font-size: 12px;
}
/* Widths declared on the header row */
th:nth-child(1) { width: 11%; } /* Last name */
th:nth-child(2) { width: 10%; } /* First name */
th:nth-child(3) { width: 20%; } /* Email */
th:nth-child(4) { width: 18%; } /* POS type */
th:nth-child(5) { width: 7%; } /* Qty */
th:nth-child(6) { width: 15%; } /* Client type */
th:nth-child(7) { width: 9%; } /* Plan */
th:nth-child(8) { width: 10%; } /* Date */
td {
word-break: break-word; /* Long text wraps */
padding: 7px 8px;
}
/* Only Date and Qty stay single-line */
td:nth-child(5),
td:nth-child(8) {
white-space: nowrap;
}
With table-layout: fixed, the browser distributes widths from the declared header values instead of calculating from cell content — a predictable, stable table with no horizontal scroll.
Lessons learned
-
Always test the Firebase URL in the browser before writing fetch code. Opening
https://<db>.firebasedatabase.app/<node>.jsondirectly reveals the node, data structure, and exact key names in seconds. - Never guess the key names of an external DB. "Intuitive" names seem obvious until the DB uses something else entirely. Always verify from actual data.
-
setTimeoutto wait for the DOM is fragile;requestAnimationFrameis deterministic. AsetTimeout(fn, 50)"to give the DOM time to update" is a red flag — the delay works on your machine but may not on a slower device or under rapid re-renders. -
Dynamic container IDs are dangerous when re-renders happen. An ID based on
Date.now()generates a new identifier every call; any code referencing the "old" ID hits a dead end. Fixed, semantic IDs eliminate this entire bug class. -
table-layout: fixedwith percentage widths is the right call for responsive tables — letting the browser calculate from content leads to unpredictable results.
Full write-up on my blog: roversia.it/blog-05-paypos-firebase-admin.html
Top comments (1)
I've seen similar issues with date-based IDs, how did you handle potential ID collisions with the Date.now() fix? I'd love to swap ideas on this, following for more Firebase debugging tips.