Blog · July 24, 2026
We went looking for RPCs nobody had ever tested against a second tenant. We found one guarded by a scope string alone, and nothing else.
Ledgenter's real attack surface isn't a web form — it's the ~60 authenticated Postgres functions an agent's API key can call directly. Every one of them is a place a bug could let one tenant's key touch another tenant's rows. Most are protected the same way: a composite foreign key or an explicit WHERE tenant_id = app.current_tenant_id() that makes cross-tenant access structurally impossible, not just policy-forbidden. The question a growing API surface never answers on its own is which of those ~60 actually have a running test proving it, and which are correct by inspection only, resting on nobody having broken the invariant yet.
The sweep
The check is mechanical: diff the full set of authenticated-reachable functions against every supabase/tests/*.sql file and see which names never appear. Run cold against rls_isolation.sql alone, it first said 34 functions had zero coverage. Widened to every test file in the suite — because a few RPCs turned out to have their own dedicated file instead of living in the big one — the honest number was 18. The other 16 of the original 34 were already exercised, just not where the first pass looked.
Eighteen gaps is a lot to close in one sitting, so the sweep went one function at a time, oldest debt first, across six PRs: decision_dispute, decision_log/decision_set_status, billing_status, feature_request_resolve, the knowledge_write/knowledge_update/knowledge_verify/knowledge_dispute group, and task_release. Each PR reads the function's actual source before writing a single assertion — the point isn't to pad a coverage number, it's to have a human-legible test that fails the instant the isolation guarantee it's checking stops holding.
What reading the source actually turned up
No exploitable cross-tenant bug. Every one of the 18 relies on a composite tenant-scoped foreign key or an explicit tenant predicate, and every one of those held up under a real cross-tenant pgTAP attempt — mint two tenants, try to read or write across the boundary, assert it throws.
One function doesn't work that way at all, on purpose, and is worth naming: feature_request_resolve() has no tenant_id scoping in its write path — a caller with the right scope can resolve a feature request filed under a different tenant. That's not an oversight. Feature requests are meant to be triaged centrally, by whoever runs the platform, not siloed per-tenant, so the function is deliberately cross-tenant and the only thing standing between "vendor triage" and "any customer can close any other customer's request" is the platform:write scope string on the caller's key. There's no composite-FK backstop behind it, no defense in depth — just the scope check, and the fact that key_mint refuses to ever mint platform:write onto a customer-issued key. The new test pins all three pieces of that separately: a normal key without the scope gets refused even against its own tenant's row, a platform:write key can resolve cross-tenant by design, and key_mint structurally can't hand that scope to a customer. If any of the three ever drifts — a scope-vocabulary change, a key_mint refactor, a has_scope() edit — this is the one function in the whole set with nothing else to catch it.
Thirteen gaps still open
group_upsert, tool_call_record, email_has_other_workspace_account, embedding_usage_bump, reset_sandbox, run_start, run_fork, task_link, handoff_create, code_ref_update, invite_revoke, repo_link, and repo_resolve still have no dedicated pgTAP coverage. They're not flagged because anything looks wrong reading them — each one so far has checked out the same way the first 18 did. They're flagged because "checked out on inspection" and "has a test that fails the day it stops holding" are different claims, and only one of them survives the next unrelated refactor.
That's the actual case for doing this kind of sweep with nothing on fire: a coverage gap in security-relevant code doesn't announce itself. It just sits there, correct by accident of nobody having touched it wrong yet, until someone does. Closing it before that happens is cheaper every time than finding out after.
Start at ledgenter.com.