Urgent.News

What's breaking now, across thousands of outlets.

Tech

FinanceHub #2: Cuando el código compila pero igual está mal

Introducción En el artículo anterior conté cómo decidimos migrar FinanceHub de una plataforma no-code a un backend propio, y que la parte difícil no fue escribir código: fue decidir cómo construirlo. Dije que antes de pedirle cualquier tarea a un agente de IA, definimos la arquitectura, el modelo de datos, el contrato de API y un plan por fases. Esta es la parte de cómo eso se sostuvo en la…

Original Spanish Read in English

In the previous article, I detailed how we decided to migrate FinanceHub from a no-code platform to our own backend. The hard part wasn't writing code; it was deciding how to build it. Before delegating any task to an AI agent, we defined the architecture, data model, API contract, and a phased plan. This is how it played out in practice and what we discovered when it was put to the test.

Our fundamental rule was simple but hard to follow under pressure: the API contract (openapi.yml) and the database schema were treated as fixed inputs, not something to be improvised upon encountering gaps. In particular, if a field wasn't in the contract, it didn't exist in the DTO. We followed a contract-first protocol: any change in shape of an endpoint first updated openapi.yml, was explicitly announced, and only then was code written.

The entire plan was written in advance, decomposed into phases and numbered tasks, each limited to approximately one domain entity and its CRUD, with a verification step decided before starting.

Living rules were written in files ( .claude/rules/*.md ) for anything a new person in the codebase would get wrong if not explicitly told. This included which fields were calculated by triggers and never accepted in a request, the exact pagination format, and the standard error envelope. This wasn't exotic; it was essentially spec-driven development applied intuitively before we knew how to name it. What changed the game was who executed the plan: an AI agent, task by task, against that fixed contract.

Compiling and tests passing was never the yardstick. With most of the code written by the agent, the question shifted from "does it work?" to "how do I know?" The answer was a verification check of 6 steps for each task, always in the same order: compile cleanly, run the test suite against Postgres, launch the full app, obtain a JWT emitted by Supabase (never a synthetic stub), test the endpoint manually with happy path, validation, ownership, 401/404, and finally clean up test data.

This step was particularly important. It seemed like a detail, but it wasn't. What appeared when testing against the running system was that the code compiled, the agent's explanation sounded reasonable, yet it was still wrong. For example, a login that failed silently. Supabase signs its JWTs with ES256. The default decoder of Spring Security Resource Server only trusts RS256.

Every token emitted by Supabase was rejected with a silent 401, rather than crashing or showing red, simply nobody could enter. This was detected because the JWT was required by step 4, not one fabricated for the test to pass. The PATCH that deleted data. A partial update overwrote any optional field in the body with null. It's the type of bug that never appears in a test that sends a complete object, and only shows up when a client sends a partial payload, like any form submission.

There were 24 security policies protecting nothing. The project had row-level security enabled on 20 tables, with 24 well-written policies. All were dead code: Postgres denied access at the GRANT level of schema/table before evaluating any policy, and none of the custom schemas had that GRANT given to non-privileged roles that RLS was supposed to protect.

None of this showed up in a unit test, in mvn test, or by reading the policies one by one - it required actually logging in as the restricted role and confirming that even the user's own data returned "permission denied" instead of filtering correctly.

The report that lied according to the time zone. A monthly summary view aggregated with date_trunc( month , columna_timestamptz). In UTC, that's invisible. In the project's timezone, each transaction was reported silently under the previous month. This looked correct from reading the SQL, from the code, as long as you didn't compare the numbers against a known result in advance.

What this means is none of these bugs were exotic or hard to explain once found. What made them dangerous was that each one passed any superficial review: the code compiled, the logic sounded reasonable when read, and even appeared correct in more than one case. An AI agent can write a lot of code, and can sound convincing explaining why that code is good.

But the agent saying it's good was never treated as sufficient - nor were the tests green, if those tests were run against a stub instead of the running system. The verification discipline did not replace the agent. It decided what evidence counted as proof that something worked, and what didn't. This is just the beginning. With the backend built and verified, the frontend brought a different class of problem: there was no new spec to follow in each step, but an existing system to maintain honestly as new facts appeared. The next article will cover that.

Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.

Read the original at dev.to →

More in Tech

More from Saturday 26 September →