Approval Decision Audit-Trail and Atomicity Automation: An Indexed Column No Write Path Ever Filled

An index is a promise that someone intends to ask this question later
Building a dedicated database index for a specific column is not a neutral, cost-free decision — it is a deliberate signal that someone expects that column to be queried, looked up, filtered, or reported on later, and is willing to pay the ongoing cost of maintaining that index to make those future queries fast. An index on an empty column is a promise nobody kept: the infrastructure to answer a question was built and maintained, while the one thing that would let it actually answer that question — a value ever being written into the column at all — never happened.
How the underlying problem shows up before you fix it
A table carries a column, and a dedicated index on that column, clearly intended to support looking answers up by it — and no code anywhere in the system ever writes a value into it.
More than one independent code path writes decisions into the same table over the table's lifetime, and confirming the gap requires checking EVERY path, not just the most recently touched one — a column can look "obviously populated somewhere" simply because it clearly should be.
A single user-facing action (approving or rejecting one item) is implemented as a sequence of separate database reads and writes, run one after another from application code, with no database transaction wrapping the sequence as a single unit.
The step actually protecting against two people deciding the same item at once is a single conditional update — real protection for that one specific race, but no protection at all for consistency across the OTHER reads and writes in the same sequence.
The absence of a value in an audit-oriented column produces no error, no failed report, no crash — a report that would have used it simply omits an answer nobody was checking for, indefinitely, until someone goes looking specifically for that column's own history.
How three independent write paths shared one gap, and how a single locked function closed it
The client-approvals table's own schema — captured in a dedicated reference snapshot specifically because the table itself predates the project's migration history — has carried a column recording who responded to a given decision, with its own purpose-built index, for the table's entire life. Confirming whether that column was actually ever populated meant checking every place that ever wrote a decision into the table, not assuming the most obviously relevant one had it covered: the approval screen's own direct database write (which set only the decision's status and the time it was made), the edge function handling calendar and deliverable decisions (thirteen hundred lines, searched directly for the column's own name — zero matches), and the separate service handling batch planning-day approvals (searched the same way — zero matches). Three independently written, independently maintained code paths, spanning years of the table's use, all wrote decisions into the identical table, and not one of them had ever set the one column specifically built and indexed to record who made the decision.
The same investigation, tracing exactly how a single approve or reject action actually executed, found the decision itself was never a single atomic operation. Approving one post ran as an ordered sequence of database calls from application code: read the approval record to find the related task, write the approval as approved (protected only by a check that it was still pending, at the time), read the workspace's own configured service-stage name, read the related task's own details, write the task's stage and status, and — separately, best-effort — insert a notification for the internal team. Six steps, two tables, zero of it wrapped in a shared database transaction, and no row lock held across any of it beyond the single conditional update in step two. That specific update genuinely protected against the one race it was built for — two people deciding the same item within the same instant — but nothing protected the OTHER five steps from running inconsistently with each other if any one of them behaved unexpectedly.
The fix addresses both findings with the same structural change: one dedicated, security-scoped database function per decision type (a standalone post, a planning day, a batch of planning days, a finished deliverable, an internal review). Each function opens by taking an explicit lock on the specific approval row before evaluating anything else — nothing else can act on that same row until the function finishes. Every related write for that one decision — the approval's own status, the connected task or content item, and now the identity of whoever actually made the call — happens inside that same function, as a single transaction: it either all commits together, or none of it does, closing the six-step, two-table sequence into one atomic unit. And because the responding identity is available directly, inside the database, at the exact moment the decision is being recorded, the column that had sat indexed and empty since the table's own creation is finally set — not as a separate afterthought bolted onto the old flow, but as one more field written by the same atomic operation that decides everything else.
What is actually built today
A dedicated database function per decision type (single post, planning day, batch of planning days, finished deliverable, internal review) performs the ENTIRE decision — status change, connected record updates, and who-responded — as one atomic transaction.
Each function takes an explicit row lock before evaluating or changing anything, closing the exposure window that previously existed across a six-step, un-transacted sequence run from application code.
The column recording who responded to a decision — present in the table's schema, with its own dedicated index, since before this fix — is now set by every decision path, for the first time in the column's own lifetime.
A batch decision (approving every remaining planning day in a month in one action) runs as a single request that evaluates each item through the identical per-item function, skipping anything already decided rather than failing the whole batch on one already-answered item.
An approver can now leave a written comment as part of approving an item — a new column added by the same migration, something the previous schema had no field to hold at all.
The frontend no longer performs any of the multi-step orchestration itself — it calls the appropriate decision function once and reads back a structured result, rather than sequencing reads and writes across two tables from application code.
A decision recorded, with nobody able to say who made it (illustrative framing of the actual measured finding)
A team lead reviews a client's approval history to understand a recent dispute over who signed off on a specific post. The approval record clearly shows the item was approved, and when — the column built specifically to record who is sitting right there, indexed for exactly this kind of lookup. It is empty, and it has been empty for every decision ever recorded in that table, regardless of which of the three code paths made it, because none of them had ever set it. After the fix, the identical lookup on a new decision returns a real answer, because the column is now set by the same atomic operation that records everything else about the decision.
What changes operationally
Every client approval decision in Centriu Axis now runs as a single, row-locked, all-or-nothing transaction instead of an unprotected multi-step sequence across two tables — and, as part of that same write, a column that had been present and indexed on the approvals table since before this fix, but never once populated by any of its three independent write paths, is finally set on every decision going forward.
When this is not the right fit
This automation governs how a single approval decision is recorded internally — its atomicity and who it attributes the decision to — it does not change what an approver is shown, what they are asked to decide, or who is authorized to make a given decision (a separate, independently fixed mechanism in the same commit).
A schema built for a question vs. code that never answers it
A database column and its own dedicated index represent a real, ongoing cost — every write to the table maintains that index whether or not the column itself is ever filled in. Carrying that cost while never actually writing the value is the specific, quiet failure mode this fix closes: not a missing feature nobody thought to build, but infrastructure that was built, deliberately, and then silently bypassed by every single code path that could have used it, for as long as the table existed. Closing that gap took checking each of the three write paths individually against the column's own literal name, not trusting that a column with an index this specific must already be in reliable use somewhere.
Related systems
Main system: Centriu Axis.
What it does NOT do
- Does not recover who responded to any decision made before this fix shipped — the column being newly populated applies to decisions made from this point forward; earlier rows remain exactly as they were recorded.
- Does not change who is authorized to approve or reject anything — that is a separate authorization rule, fixed independently in the same underlying commit, covered on its own page.
- Does not change what an approver sees or is asked to decide — the fix is entirely about how the decision is recorded and attributed internally once it is made.
- Does not add new approval stages or decision types — the same five decision types (post, planning day, batch, deliverable, internal review) that existed before are the same five now running through the new transactional functions.
- Does not remove the specific single-row protection that already existed against two people deciding the same item at once — that protection is preserved and extended, now backed by an explicit row lock rather than a conditional update alone.
- Does not replace an agency's own audit or compliance process — it makes the underlying attribution data exist and stay consistent; how a team uses that data for its own reporting is a separate decision.
Security and governance
Every client-approval decision in Centriu Axis is now recorded through a security-scoped database function that locks the affected row before writing, performs every related change as a single atomic transaction, and attributes the decision to the responding identity as part of that same write. Any client or business data referenced remains subject to Brazil's LGPD (Law No. 13,709/2018). Full detail on access control lives at /governanca and /iso.
Pricing and contracting
Included at no extra cost with any Centriu contract. Values and terms come from the official pricing table at /precos (Centriu's central source — never restated here).
Frequently asked questions
Was the "who responded" column really never populated?
Confirmed directly by searching each of the three independent code paths that ever wrote a decision into the table for the column's own name — zero matches in any of them, despite the column and its own dedicated index existing on the table the whole time.
Why does an unused, indexed column matter?
A dedicated index is an ongoing cost paid on every write to the table specifically so that column can be looked up quickly later — paying that cost while the column is never actually filled means the underlying question it was built to answer could never be answered.
What does "the decision was never atomic" mean in practice?
Approving or rejecting a single post ran as up to six separate database reads and writes across two tables from application code, with no shared transaction and no row lock protecting the sequence beyond a single conditional update.
What is the fix?
One dedicated database function per decision type now locks the relevant row, performs every related write as a single all-or-nothing transaction, and records who responded as part of that same write.
Does this recover historical attribution data?
No — decisions made before this fix have no responding identity to recover; the column is populated going forward, starting with decisions made after the fix.
What does Centriu Axis cost?
It is sold by subscription with a published starting price — exact current values are on the central pricing page.
See how Centriu Axis makes every approval decision atomic and attributed
Reach our commercial team directly, or leave your details below — we'll follow up with guidance for your case.