Drafted with AI assistance, edited and checked line by line against a real build before publishing.
On the Soulful Ledger backend build, I deliberately broke the tenant scoping clause in one query, just to see what would happen. The test suite stayed green. Every test passed. That's the moment I learned a green test suite proves nothing on its own. It only proves something once you've watched it catch a real break, and then fixed the break back.
That single failure is where most of what follows came from. Not a framework borrowed from somewhere else. These are wounds, distilled into defaults, from building a real multi-tenant system with AI agents doing a lot of the typing.
Every query is parameterized, no exceptions
$1, $2 syntax, never string interpolation into SQL. Not "just this once for the admin panel." Not "just for this one internal script." The exception is always where the injection ends up living, and an AI agent generating code at speed will happily write the unsafe version if you don't tell it not to, every single time you ask.
I watched this happen on an early Soulful Ledger endpoint. An agent building a quick internal reporting query dropped a raw f-string straight into the WHERE clause, because it was faster to write and nothing in the request had mentioned parameterization. Caught it in review, not in production, which is the only place that catch is allowed to happen.
Tenant scope never comes from the client, and never from the model
If a multi-tenant boundary exists, the query enforcing it needs the scope value baked into its WHERE clause, computed server-side, before the query ever runs. Not from a request body. Not from a URL parameter. Not from anything an AI model outputs, even if the model is only echoing back a value you gave it three turns ago.
Here's the test that actually proves it: deliberately break the scoping clause, confirm the suite catches it, then restore the clause. That's the test I ran on Soulful Ledger, the one that came back green with a broken clause still in place. Twelve passing tests told me nothing until I watched one of them fail on purpose.
AI never gets raw access to the data it's protecting
If an AI needs to answer questions from a database, it doesn't get a query tool. It gets a fixed, named, reviewed set of read-only functions, built the same way a human-reviewed API endpoint would be built. The tool schema exposed to the model never includes a scope or tenant field. That value gets injected server-side, after the model has already made its choice, never accepted from the model's own output.
The agent writing the code doesn't hold the keys
An AI coding agent never carries a database owner or superuser credential across a session. It can generate the migration. It can tell you exactly what command to run. The human runs anything elevated directly, by hand. The agent works with the same restricted, least-privilege credential the running application itself uses, nothing more.
This shows up in small decisions too. When I built the security audit stack for client engagements, the exploitdb server runs stdio-only, no HTTP port, no API key sitting around to leak. Canopy Guard, the free audit tool, stays external-only and uncredentialed on purpose, a completely separate boundary from anything that touches a client's real environment. Two different products, two different trust levels, and the line between them isn't an accident.
A green test suite is not proof of anything
Every security-relevant guarantee needs a test that could actually fail, and most teams never check that it can. Twelve passing tests prove nothing by themselves. What proves something is deliberately introducing the break the test is supposed to catch, watching the right thing go red, then fixing it.
The stronger version of this is proving the negative. "This works for the right tenant" is a weak claim. "This returns nothing for the wrong tenant, even when the wrong tenant's data would rank as the best possible answer" is the claim worth building a test around. If only one file in your codebase should ever query a sensitive table, write a test that fails the moment any other file does. That's a chokepoint test, and it catches the mistake before it ships instead of after.
Where this actually leaves you
I still run the client work, the builds, the calls, same as before any of this was written down. None of it changed the pace. What changed is that I don't report something as done anymore without having actually run it and watched it behave correctly, and I don't let a large pile of adjacent, verified work stand in for testing the one thing that hasn't run yet.
This approach costs time. It's slower than trusting the first green test suite you see. It's also the only way I've found to know the AI-assisted parts of a build are actually safe, instead of just hoping they are.
Top comments (0)