TATrương Tuấn AnhTuna / truongtuananh.vn
All posts
date written
minutes to read
6
sources cited
6

The database was empty and the screen still showed 1,000 projects

On 17 July 2026 the production database was empty across all 20 tables and the screen still showed 1,000 projects. And 300 customers, all of it from the sample data set. Why my first rule is that every number on screen must trace back to a real query.

Topics:
  • erp
  • data
  • ai with a human approver

On 17 July 2026 I sat down and counted every table in the live NoLimit ERP database. All 20 tables came back 0. Status table 0, projects 0, customers 0, users 0. Account count 0, meaning nobody could log in yet. The web app and the bot had gone live earlier and were serving real traffic.

The problem: open the projects, customers, profit and loss, and revenue pages and I saw 1,000 projects and 300 customers, filled in like the real thing. The sample data banner was off. An empty database with a screen full of numbers.

The incident: empty database, screen full of numbers

It came down to one line in the data access layer. The function read the status table from Supabase, and when it found nothing it fell back to a data file sitting in the source tree. That file was exactly the 1,000 project set I had built to preview the interface on a developer machine with no connection configured.

That made sense when it was written. A developer machine without Supabase reads the sample file, and everyone understands the numbers are fake. But production had Supabase, and its status table was empty because no data had been loaded yet. Which was the truth that day. The code collapsed "no database configured" and "database configured but empty" into one case. Both landed in the same fallback branch.

One more factor: the environment variable that picks the data set was blank in production, so it defaulted to the sample set. The application served that invented set of 1,000 projects and 300 customers as though the numbers were real.

On the go-live sequence: item 2 (create 3 accounts) and item 6 (load the data) on the checklist were skipped, while item 7 (deploy) was done. That was my operational mistake. It should have produced an empty screen, and stopped there.

Why the sample data banner stayed quiet

The application already had a banner to tell viewers the numbers were fake. The flag that raises it turned on only when the Supabase connection URL was missing. Production had that URL. So the banner stayed off.

Two conditions out of step. The data source fell back on the test "status table is empty". The banner turned on with the test "connection URL is missing". When two tests look at two different facts, sooner or later fake numbers run without a label. I call this a silent wrong: clean logs, green build, a page that renders nicely. Only the numbers are invented.

Worse, the viewer had no way to check. A director who opens the management dashboard and sees 1,000 projects will believe that is what the system holds. When the screen looks fine, nobody goes back and counts the database.

The fix: once the database is connected, empty means empty

The patch changed the principle: if the connection URL is present and the status table has no rows, return an empty structure instead of the sample file. Metrics at 0, empty lists, with a note that the status table has no data yet. The pages show an empty state that matches reality.

The sample set is now allowed only in the first two cases below, and both are deliberate. The third row is the empty state:

CaseData sourceSample data banner
Developer machine, no connection configuredSample setOn
Demo mode turned on deliberatelySample setOn
Production connected, status table emptyEmpty state (metrics 0, empty lists)Off, because 0 is the true number

The key point: after the patch, the flag that permits the sample fallback and the flag that raises the banner are computed from the same pair of conditions. Fake numbers always carry the sample data banner. On the read path every fake number is labelled; the write path was still unfinished, as the next section describes.

I also wrote the reason directly above the code, pointing at the project rules file: numbers on screen must come from a real query. So that a later Claude Code session reading the file understands why the block is there and leaves it alone, instead of tidying it back to the old shape.

One day later, a review found another path

On 18 July 2026 I ran 14 parallel AI passes over the code, one per functional area, with a second adversarial pass on every high-severity finding trying to knock it down. The result on this part: the read path patch is correct, but the write path still falls back to the sample set when the status table has no data. One path patched is half the job. The same assumption, no data means use the sample set, lived in more than one place.

Once a fallback branch exists in a codebase, it gets copied. Block it on the read path and you have to sweep every other place doing the same thing, then verify with an independent review before trusting the first patch.

Why this has to be rule number 1

The project rules file has a line: numbers on screen and in the bot must come from a real query, and every fake trend line and placeholder figure has to leave the screen. Before the incident I read that as an interface rule. After it I read it wider: every number a user sees must have a path back to a real query. No source, empty screen.

The same principle governs how AI works inside the ERP. AI only writes drafts, each draft carries a watermark reading "AI draft, needs approval", and it becomes data only after a person approves it. The watermark and the sample data banner are the same device: a mark for content still in draft. An unlabelled invented number and an unmarked AI draft are dangerous in the same way. They look like something that has been checked, so people believe them on sight.

Even the system description file follows the rule. Its first line reads "every figure in this file comes from the current code or from counting the live system directly, and every rewrite means counting again". Its last line reads "if you are reading this later, count again before you believe it". The reason: an earlier version of that same file described the project wrongly.

What I took from it

  • A sample data fallback is only safe when a label always travels with it. The flag that picks the source and the flag that shows the label must be computed from the same condition.
  • "Connected but empty" is a state of its own, separate from "not connected", and it needs an empty screen of its own.
  • One path patched is half the job. The same assumption usually sits in several functions; sweep them all, then have someone else read it back.
  • The rule "every number traces to a real query" covers numbers on screen, AI drafts, and the system documentation alike. No source, leave it empty.

If your company runs on Zalo, the messaging app most Vietnamese businesses use, plus Excel, and you want an ERP where every number on screen traces back to its source, tell me the context.