Skip to content
SL
←Back to the notes

2026-09 · 6 min read · Serge LibockDraft

The bug that hid behind the admin key

A compliance SaaS built with AI tools passed 273 of 276 tests and could not load a single page for a real user. The defect was two missing words, and nobody had seen it because every test bypassed the security layer.

Earlier this year a founder sent me a codebase and a handoff document. The product was a compliance SaaS for money services businesses: upload transactions, detect the ones that cross a regulatory threshold, generate the reports, get them approved, submit them. He had built most of it with AI tools over a few weeks. The handoff said row-level security was in place, the threshold engine was done, and about 280 tests existed. He wanted someone to finish it.

I was hired for a verification phase first, before touching a feature. The rule I set for myself was simple: nothing in the document counts until I have run it. Not because the document was dishonest, but because it had been written by reading the code, not by executing it, and those are different activities.

What running it showed

Installing the dependencies had never been done. Type-checking produced a list of errors, most of them mechanical. The database migration did not apply on a fresh PostgreSQL because of function ordering and a generated column. The seed data used identifiers that were not valid UUIDs. None of this was alarming; it is what a never-assembled project looks like, and the fixes were small.

Then I ran the tests. The threshold engine passed 83 of 83. The data layer passed 147 of 149. The regulator adapter passed 43 of 44. On paper, a healthy project.

Then I logged in as a user.

Every query failed. Not one page loaded. The error was stack depth limit exceeded, PostgreSQL’s way of saying a function has called itself until the stack ran out.

Why

The row-level security policies were real and well written. Each one asked a helper function which organisation the current user belonged to. That helper read the users table. Reading users triggered the policy on users, which called the helper, which read users, which triggered the policy. Forever.

The fix was two words. The two helper functions needed SECURITY DEFINER, which lets them read the table with the function owner’s rights instead of re-entering the policy. Half an hour, including the time to prove it.

Why nobody had seen it

Every server-side path and every integration test connected to the database with the administrative service-role key. That key bypasses row-level security entirely. So the application worked perfectly in every path that was ever exercised, and no genuine logged-in user could have loaded a single page. The integration tests named “tenant isolation” were testing the application’s own filtering, not the database’s enforcement, because they never went through the door a real user goes through.

The handoff document had described RLS as verified. It had been verified by reading. The policies existed and were correct. They had never been executed by a non-admin, and so the one defect that mattered was invisible.

What I took from it

Three things, none of them about AI specifically.

A test that bypasses your security layer tells you nothing about your security layer. If tenant isolation matters, at least one test has to authenticate as a tenant and try to read another tenant’s rows. After the fix, I did exactly that: as one client’s user, I could see my two transactions and not the other three, my one client record and not the other four, and zero of a competitor’s data. That table is now in the report, and it is the only evidence of isolation that means anything.

“Verified” is a claim about execution, not about reading. Code generated by a model reads well. It is fluent, it is commented, it looks like the work of someone careful. That is exactly why reading is not enough; the fluency hides the fact that nobody has ever run it end to end.

The defects that matter are usually small. This project was not fundamentally flawed. It was closer to “never assembled” than “wrong”. The critical bug was two missing words; the rest was eleven to sixteen hours of mechanical fixes, which is what I told the client, with the list, before we agreed on the next phase.

The product later ran end to end in the browser, client portal and admin portal, with a second round of eight defects found the same way: by using it as a person rather than as a script. It did not reach production; the founder paused before launch. But the pattern is the one I now expect from any codebase that a model wrote and a human has not yet run: good bones, one fatal detail, and a document that is more confident than the code.

If you have one of those, the first thing to buy is not features. It is a week of someone running it.

Contact

Open to remote & contract work. Fintech, regulated systems, applied AI.

I’d like to hear about the hard part.

I work from Yaoundé and keep 09:00 to 15:00 Montréal time free for calls. I reply within a working day, in French or English. Or write to libockserge.dev@gmail.com

Yaoundé––:––Montréal––:––Paris––:––

© 2026 Serge Junior Libock · Yaoundé, Cameroon

SLPortfolio · 2026

Serge Junior Libock

Technical Lead & Software EngineerBanking systems and applied AI

Setting type000%