Client Approval Authorization Consistency Automation: Approved On Screen, Still Pending in the Database

A rule written twice agrees on almost everything, and diverges on exactly the case nobody was watching
Two independently written copies of the same authorization rule rarely disagree on the easy cases — a master account is always allowed, a disabled team member is always denied, an unrelated client is always denied. Where two independent copies of a rule actually diverge is the edge case each author had to make a judgment call about without a specification to point to: what happens when the specific permission being checked was simply never recorded for this person at all. One author reasonably reads silence as "nothing was granted, so deny." Another, just as reasonably, reads the same silence as "nothing was ever taken away, so allow" — especially for a person who was invited before the fine-grained permission screen existed to record anything at all. Both readings are defensible in isolation. Only one of them can be correct once the exact same person hits both rules in the same afternoon.
How the underlying problem shows up before you fix it
The rule for who may approve is implemented independently in more than one place — a database security policy and an edge function's own mirror of the same logic — with no single function either one calls to guarantee agreement.
The two implementations agree on every explicit case (a recorded `true`, a recorded `false`) and diverge only on the unrecorded, absent case — precisely the case most likely to be common and least likely to be tested.
A denied database write returns success with zero affected rows rather than an error — a shape specifically designed to let a legitimate double-click or a genuine race between two approvers fail silently and safely.
The exact same silent, zero-row, no-error shape is indistinguishable, from the calling code's point of view, between "someone else already approved this half a second ago" and "the security rule just refused your request outright."
A frontend success handler that fires on a resolved promise, without separately checking how many rows were actually affected, shows a confirmation message regardless of which of those two very different things just happened underneath it.
How a click that looked like it worked left the real status untouched — and the single predicate that ends the disagreement
The approval screen's own "Approve" and "Reject" buttons wrote directly from the browser to the underlying approvals table, protected only by the database's row-level security policy — no edge function sat between the click and the write. That policy's rule, confirmed directly from the fix's own inline comment describing the exact behavior it replaces, required the specific approval permission to be recorded as explicitly true; if it was never recorded at all, the policy treated the write as unauthorized and matched it to zero rows, exactly as row-level security is designed to do.
A separate authorization check, written independently inside the edge function used for the same product's calendar and deliverable decisions, encoded the identical underlying rule — but resolved the exact same "never recorded" case the opposite way: absent meant allowed, on the reasoning that an approver invited before a permissions panel even existed to record anything should not lose access purely because nobody ever explicitly re-confirmed it. Both readings appear in the product's own code comments as deliberate, reasoned decisions — made independently, at different times, by different code paths, with no shared function connecting them.
The consequence surfaces at exactly the point these two disagree: a client approver with the permission never explicitly recorded either way clicks "Approve" on the direct-write screen. The database policy denies the write and returns zero affected rows — no exception, no error, because a genuine zero-row result is also exactly what a legitimate, benign race between two people approving the same item at once looks like, and the calling code's own comment says as much: an update affecting no rows means someone else already won that race, so do nothing further and don't show an error. The screen's own success handler, which fires whenever the underlying request resolves without throwing, has no separate check distinguishing those two cases — it shows the same confirmation either way. The approver sees "Post approved successfully." The post's actual status never moved past pending.
The fix collapses both independent copies of the rule into one: a single predicate function that takes an organization and a client, and returns whether the calling, authenticated identity is allowed to approve for that pairing. The database's own security policy calls it directly. Every edge function calls the identical function too, through a small shared helper that authenticates as the CALLER (not as an administrative service account), so the database resolves the exact same identity the exact same way regardless of which code path asked. The unrecorded-permission case is now decided once, consistently, in the direction the edge function already used — an approver invited before permissions existed keeps working. And because decisions now run through dedicated functions that check this same predicate explicitly and raise a stated, human-readable refusal when it fails, a genuinely denied approver gets an actual, visible error message instead of a silent, indistinguishable false success.
What is actually built today
One predicate function decides who may approve content for a given organization and client — called directly by the database's own row-level security policy, with no separate copy of the logic living anywhere else in the database layer.
Every edge function that needs the same answer calls that identical function through a shared helper, authenticated as the calling user's own identity — not a separate, independently maintained mirror of the rule.
The one case that previously diverged — no permission ever explicitly recorded for a given approver — now resolves the same way everywhere: permitted by role, matching what the edge function already did before this fix.
A denied approval attempt now raises an explicit, stated refusal from inside the decision function itself, rather than silently matching zero rows the way a direct database write always could.
A JavaScript fallback mirrors the same rule for the narrow window before the underlying database function exists in a given environment, so a mid-rollout deploy cannot silently block every approval outright.
No change to who is DENIED under either previous rule's strict reading — master accounts, active staff, a client's own recognized approvers, and legacy client logins are still exactly who is authorized; the fix closes the one case where the two rules genuinely disagreed.
A confirmation toast, and a post that never moved (illustrative framing of the actual measured finding)
A client contact, added as an approver before the agency's team ever opened the fine-grained permissions screen for that account, opens their approval queue and clicks "Approve" on a post. Before the fix, the click resolves without error, a "Post approved successfully" message appears, and the queue refreshes — while, underneath, the database's own security rule silently rejected the write because no explicit permission had ever been recorded for that specific approver, and the calling code's own race-condition handling treated the zero-row result as a normal, silent non-event. The post remains pending, invisibly, until someone else — staff, an admin, or an approver whose permission happens to be explicitly recorded — eventually approves it through a different path. After the fix, the identical click is evaluated by the one shared predicate, resolves as permitted (matching the edge function's own long-standing default for this exact case), and the post's status genuinely changes.
What changes operationally
Centriu Axis's database security policy and every edge function that decides who may approve content now call the identical predicate function, authenticated as the same calling identity either way — closing a gap where the single most common unrecorded-permission case resolved two different ways depending on which code path handled the click, and where a denied write on one of those paths was visually indistinguishable from a genuine success.
When this is not the right fit
This automation governs the internal consistency of Axis's OWN authorization rule for who may approve content, and how a denial is now surfaced — it does not change who your agency has decided should be allowed to approve, does not add new approval roles or permission types, and does not retroactively review approvals recorded before this fix shipped.
One rule enforced from one place vs. the same rule copied and re-decided
Writing an authorization rule once and re-implementing it independently wherever it's needed feels harmless when both authors are being careful — the actual defect here was never a case of either implementation being obviously wrong in isolation, both were reasoned, deliberate, defensible choices about an edge case with no shared specification. That is precisely the risk of a copied rule: it doesn't take carelessness to diverge, it only takes two different people making two different reasonable judgment calls, months apart, about the one case neither of them had a clear answer for. A rule called from a single, shared source cannot diverge from itself.
Related systems
Main system: Centriu Axis.
What it does NOT do
- Does not change who your agency has configured as an approver, or grant any new approval role — it makes the existing rule for who qualifies resolve identically everywhere it's checked.
- Does not retroactively correct any approval decision recorded before this fix shipped — a team concerned about a specific past item should review that item's own approval history directly.
- Does not remove or weaken the database's own row-level security on the approvals table — the same table remains protected by row-level security; only the RULE that security policy calls was unified.
- Does not change what happens for any approver whose permission was already explicitly recorded, in either direction — the fix is scoped to the specific case where nothing was recorded at all.
- Does not add a new approval type or workflow — the fix governs the authorization check underneath the existing approval flows Centriu Axis already runs (posts, planning days, deliverables, internal review).
- Does not replace an agency's own decision about which specific people should be configured as an account's approvers.
Security and governance
The database's own row-level security policy on the approvals table and every edge function that decides who may approve content now call one shared, database-resident predicate, authenticated as the calling user's own identity rather than a separate service account — removing the possibility of the two disagreeing on who is authorized. 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
Could a real approver have seen a success message without actually approving anything?
Yes — an approver with no explicit permission recorded either way could have the database silently reject the write while the interface's own success handler still fired, because a resolved request with zero affected rows and a database-level denial looked identical from the browser's side.
Why did two different rules disagree on the same account?
The database's security policy and the edge function's own copy of the rule were written independently, at different times, and made two different, individually reasonable choices about what an unrecorded permission should default to.
What is the single fix?
One predicate function now decides who may approve, called directly by the database's own security policy and by every edge function through a shared helper using the caller's real identity — there is no longer a second, independent copy of the rule anywhere.
What happens now when someone is genuinely not allowed to approve?
The decision function raises an explicit, stated refusal instead of a database write silently matching zero rows — a denial is now visible, not indistinguishable from a coincidental success.
Did this change who is allowed to approve content?
Only for the one case that previously disagreed — an approver with nothing explicitly recorded is now treated consistently as an approver by role, matching what the edge function already did; every explicitly recorded permission, in either direction, is unchanged.
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 keeps approval authorization consistent end to end
Reach our commercial team directly, or leave your details below — we'll follow up with guidance for your case.