Spec redesign / Supabase Studio
Row level security fails in silence. A wrong policy and no data look exactly the same: an empty array, and two hundred OK. This is a redesign of the screen where that starts, with a working prototype you can break yourself.
A spec redesign for Supabase · independent work, not affiliated
The film
The editor as it is today, the research behind the redesign, and the proposed editor driven live. Everything on screen is the real prototype.
The prototype
Everything in the film is live. The policy you build is evaluated by a real row level security interpreter over a sample table: grants first, then role, then the permissive union, then the restrictive intersection. Change anything and the result recomputes. Nothing is scripted.
42501 before any policy runs.Opens in a new tab. Best on a wide screen.
The evidence
I pulled every GitHub
discussion in supabase/supabase matching "RLS policy", filtered out the
show-and-tell, and kept the ones where a policy is the actual subject. Then I coded each on
its title and opening post.
| Pattern | Threads | Share |
|---|---|---|
Right policy, wrong identity. auth.uid() is null, the anon
key was used, the token came from Clerk or Auth0, or the call ran server-side with no
session. | 150 | 34.2% |
USING versus WITH CHECK. Mostly new row violates row-level
security policy on insert. | 134 | 30.6% |
| Grants, not policies. The policy is fine; the role never held the privilege. | 60 | 13.7% |
| Expressing the rule at all. Multi-tenant membership, org scoping, joins. | 49 | 11.2% |
| Policy combination. Two policies that each look right. | 47 | 10.7% |
| Silent denial, named as the problem itself. | 38 | 8.7% |
At 8.7% it reads as minor. It isn't a category, it's the delivery mechanism for most of the others. A wrong identity, a missing grant and a restrictive policy all arrive the same way, so only the people who thought to name the silence itself show up in that row.
The most upvoted RLS thread in the repository asks for a way to test a policy. Its author described debugging as shooting in the dark. In April 2026 Supabase shipped an RLS Tester as a feature preview, then paused it while they re-evaluate its direction. This piece is scoped to that open question.
You have your policy as Restrictive type it needs to be Permissive. The entire fix, in one reply, thirty-four minutes after a developer asked why a correct-looking policy returned nothing. Discussion #33500.
The redesign
The tester was the right capability in the wrong place. Verification you have to remember to visit is verification nobody does. So it moves inside the thing you were already doing.
Read from the source
Independent of any redesign,
found by reading PolicyEditorPanel at master. I'd happily open these as issues
or pull requests.
With no target role selected the editor emits to public, and says so in its
own helper text. The docs say to always name the role, and give the un-scoped form as the
anti-example. The default is the thing the docs warn against.
Six of the eight general templates carry no to clause. Two of them generate
auth.uid() = user_id with no to authenticated. The same
templates were corrected in 2024 to wrap (select auth.uid()). This is the
same class of fix, unmade.
The word "grant" does not appear anywhere in the policy editor, though grant failures are 13.7% of the coded corpus. The editor shows one half of Postgres's access-control model and no sign that the other half exists.
The constraints
Silence is correct, and still designable. A policy that filters a row hasn't errored, it has filtered. Postgres is right to return nothing. But the dashboard can tell "no rows matched your filter" from "no rows survived your policy", because it can run the query twice and compare. The honesty lives in the interface, not the wire protocol.
Reads are silent. Writes are not. A missing grant and a failed
with check both raise 42501. Only a using clause
denies quietly. The prototype models all three separately, because conflating them
overstates the problem.
USING and WITH CHECK are real semantics. One filters rows that exist, the other validates rows being written, and update needs both. Deriving that from the command and showing the result is different from hiding it. It is also what Supabase's CEO described in 2020, on an issue about an insert policy that wouldn't save.
Write previews need a transaction, not a special case. Running an insert under impersonation inside a transaction that rolls back gives a true answer with no side effects. Tables whose triggers call out are the real exception, and they're detectable ahead of time.
This was built with no access to Supabase's internals: 438 coded public discussions as the research, Studio's own open source as the design system, and Postgres semantics as the constraint set. It's what I'd do in the first ninety days inside, shown from outside first. If it's wrong in places only an insider could know, that's the conversation I'd like to have.
I'm a UX designer and researcher specialising in accessible, explainable technical experiences. Previously: GDS-standard government services at MHCLG, published at CHI 2024, and Ledgerline, an explainable SME credit underwriting sandbox.