DEV Community

Andrea Roversi
Andrea Roversi

Posted on • Originally published at roversia.it

Debugging a Firebase Admin Panel — Wrong DB Node, Bad Key Mapping and a Date.now() ID Bug

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';
Enter fullscreen mode Exit fullscreen mode

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": true on /leads does 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"
}
Enter fullscreen mode Exit fullscreen mode

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 .json URL 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);
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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 right N depends on device speed. requestAnimationFrame is 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;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Always test the Firebase URL in the browser before writing fetch code. Opening https://<db>.firebasedatabase.app/<node>.json directly reveals the node, data structure, and exact key names in seconds.
  2. 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.
  3. setTimeout to wait for the DOM is fragile; requestAnimationFrame is deterministic. A setTimeout(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.
  4. 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.
  5. table-layout: fixed with 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)

Collapse
 
frank_signorini profile image
Frank

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.