Testing RLS as the anon caller, not as the admin
The only test that predicts what a stranger sees asks PostgREST for rows while carrying nothing but the public anon key — the same request any visitor's browser makes. Sessions connected as the table owner, calls signed with the service_role key and dashboard editors all bypass or soften the very machinery you are trying to examine.
This page shows the request, explains each possible reply, and describes how the test becomes a standing check rather than a one-off. The behavioural half is automated: the probe behind ANON_TABLE_READABLE runs exactly this request against every exposed table on every scan.
Rules that check this
- criticalTable readable with the anon key
ANON_TABLE_READABLE
Why admin-session testing lies
Connect as postgres and Row Level Security is out of the conversation: the owner bypasses policies entirely, so a SELECT returning ten rows proves the data exists and nothing about who may see it. Sign the call with service_role and the same applies through the role claim. Even testing inside your app while signed in exercises only the authenticated path — the anon floor underneath it, which every unauthenticated request stands on, goes untouched. Each vantage point answers a different question; only the anon one answers "what can the internet read?"
The request
Pick the tables worth asking about — this query lists what PostgREST can see — then issue the GET with your project URL and anon key, shown synthetic as always:
-- What to test: SELECT tablename FROM pg_tables WHERE schemaname = 'public'; -- The test itself, as any stranger would run it: -- curl -s "https://your-project.supabase.co/rest/v1/documents?select=id" -- -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.REDACTED"
Reading each possible reply
Rows coming back is the loud outcome: the table answered an unauthenticated request, which is the ANON_TABLE_READABLE finding — exposure demonstrated, not inferred. An empty array proves nothing in either direction, because filtered-out and genuinely empty are indistinguishable from outside; treating 200-with-empty-array as success is the most common self-deception in Supabase audits, which is why the probe records reachable rather than clean. A permission error means policies engaged — the quiet answer you want, though check the table name before celebrating, because a typo produces the same status for less flattering reasons.
Reads are the half a stranger can demonstrate from outside. Writes need no proof-by-insertion: an INSERT policy lacking WITH CHECK is visible in the catalog, and MISSING_WITH_CHECK covers exactly that gap on connected scans, so the write side never depends on anyone writing to your database.
How the test becomes a standing check
One manual request describes this afternoon. The probe repeats it against every exposed table on a schedule, diffs against the previous run, and raises the finding with the table named and a column count — values never retained — whenever an answer changes. Alerts fire on transitions: created when a table first answers, resolved when policies engage, regressed when a fix slips back, which is the signal worth interrupting someone for.
Constraints hold on every run: GET, HEAD and OPTIONS only, private and metadata address ranges refused before any socket opens, and no signup required for the public check. RowShield is an independent product, not affiliated with or endorsed by Supabase. Point it at your deployed URL at rowshield.dev/audit and read what the anon key sees.
Frequently asked
- Is it safe to send the anon-key request against production?
- Yes. The request is a read your frontend already permits strangers to make; the test reveals what production answers rather than changing it, and it carries no credential stronger than view-source provides.
- The request returned an empty array. Am I protected?
- Unproven. Empty and filtered are indistinguishable from outside, so an empty array fits both a locked-down table and an open one that simply holds no rows — pair the request with the catalog check before concluding anything.
- How do I test the authenticated role instead?
- Sign in, copy the access token your client received, and send it as Authorization: Bearer alongside the anon key header. It works but resists scripting — sessions expire and flows vary — which is partly why continuous monitoring watches the stable anon floor instead.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit