Build1 distinct publisher2 min readPublished
Supabase's dashboard shield reports whether row level security is switched on, not whether any policy filters a row, and the only thing that separates those two states is a request sent with the public anon key.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
The project ref and the anon key are already in the bundle you ship to browsers, and the anon key is meant to be public [1]. A GET to `https://<project-ref>.supabase.co/rest/v1/profiles?select=*` with that key in both the `apikey` and `Authorization` headers therefore reproduces what an unauthenticated visitor can send, header for header [5]. The request still authenticates. It authenticates as `anon`, and from there the policies are the only thing between the caller and the rows, not the login screen and not the API route [3].
That is why two dashboard-adjacent checks miss the interesting case. `relrowsecurity` on `pg_class` is a boolean, and the source's own query sorts tables by it to find the ones sitting at `false` [12]. False means the policies on that table are inert and the `anon` role reads everything, which is what you get from a table created in the SQL editor that never had the flag set [11]. A `select` policy of `using (true)` leaves the flag true: enabled, has a policy, protects nothing [13]. The shield is answering a smaller question than the one you had. Catching the second case means reading the `qual` column out of `pg_policies` and noticing the word `true` on a table with user data [15]; the form usually intended is `using (auth.uid() = user_id)` scoped `to authenticated` [16]. Two SQL queries to reconstruct what one HTTP response states outright.
Coverage is per command, so a table can hold a careful `select` policy and nothing for `update`, which locks the read and leaves the row editable [17]. The two clauses do different jobs: `using` decides which existing rows a statement may touch, `with check` decides what a row is allowed to look like after an insert or update [18].
The post puts the read check at about ten seconds a table [7]. Three requests a table at that rate is thirty seconds, so a twenty-table schema comes out near ten minutes of curl [21]. That number transfers only if you already have the table list and the key to hand, and it excludes deleting the rows the insert test leaves behind [9].
Note what the clean result actually says. An empty array with a 200 is described as protected and empty for this caller [6], which is narrower than protected. And the evidence here is one practitioner's field pass, with four patterns covering most of what they find [19], not an incidence study. Run it against a project you own: the author is blunt that pointing these commands at someone else's is unauthorized access and a crime in most places [10].
Ranked by verification strength, evidence, and original report placement.
The Supabase project ref and anon key are both already in the client bundle, and the anon key is meant to be public.
On Supabase, or any Postgres with Row Level Security, the database is reachable from the browser by design.
What stands between a stranger and the profiles table is not the login screen and not an API route; it is the RLS policies.
The recurring failure the author reports is not forgetting RLS but RLS being on with a green shield in the dashboard while the table is still readable by anyone; those are not the same state and the dashboard does not distinguish them.
The proposed test is a curl to https://<project-ref>.supabase.co/rest/v1/profiles?select=* with the anon key in the apikey and Authorization: Bearer headers, described as exactly what an unauthenticated visitor can send.
Three outcomes: an empty array with a 200 means the table is protected and empty for this caller; a permission error mentioning row level security is also good; returned rows mean the data is public whatever the dashboard says.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 4, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
A signed-out caller collapses a correct-looking RLS policy into using (true)1 distinct publisher
build
Prisma v7 stops seeding for you, and the pooled URL will not finish the job1 distinct publisher
build
Your "Index Only Scan" Did 2,847 Heap Fetches: Covering Indexes Are a Vacuum Problem1 distinct publisher
build
A NetworkPolicy in another repo broke invoicing while every dashboard reported success1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable by the reader, confirmed by nobody
Almost nothing here has to be taken on trust: the anon key is already public by design, so any reader can fire the same request at their own project and get a yes-or-no in seconds. That is a stronger property than corroboration, and it is carrying the score, because corroboration is entirely absent — one post, one author, no vendor comment, no second account of the same failure. The Postgres mechanics stand on their own terms: with the row-security flag off, attached policies are moot, and using (true) matches every row whether or not a shield is green. What is not evidenced is the part that creates urgency — that this is a recurring failure — which rests on first-person recall of the author's own projects.
No exposure data of any kind
We can say the failure mode is real and trivially reproducible. We cannot say how many live Supabase projects are open right now. There are no scan results, no breach reports, no platform telemetry, not even a count of the author's own affected tables — this reporting supplies a method, not a measurement, and inventing a figure to fill the gap would be the exact error the post is warning about.
Tidier than the practice, not louder
Barely off zero, and the drift is toward neatness rather than alarm. The tone is unusually restrained for a security post: no breach count, no vendor blamed, and an explicit warning that pointing these commands at a project you do not own is a crime rather than a review. What flatters the reader is the arithmetic and the reach. Ten seconds per table assumes you already know your tables and have a row id handy, and it quietly omits deleting the rows the write tests create. More consequentially, the closing promise — that the unauthenticated request tells you what you actually shipped — is true only of what strangers can reach. The same table can be wide open to every logged-in account and still return a clean empty array.
A free checklist under a scanner's name
The account publishing this is called shipsafescan, and the piece signs off by routing readers back to the author's earlier application-layer checklist — the familiar shape of audience-building around a security-scanning brand. The pull is mild rather than hidden: the handle is in the URL, there is no product to buy inside the post, and the advice asks you to trust your own curl output rather than any tool's verdict. The mild-but-real part is topic selection: a scanner's audience is best grown by demonstrating that dashboards lie.
Mechanics solid, prevalence unaudited
Middling, and for one reason: a single dev.to post is the whole record. What keeps it from sitting lower is that the testable parts test out — the relrowsecurity flag, the qual column, per-command policies, the service role key bypassing all of it. What holds it down is that the claims a reader would act on urgently, namely how often this happens and how little time checking costs, are unverified self-report, and no one has asked Supabase whether the dashboard is meant to convey what the author says it fails to convey.