Free tool · no signup

Can a stranger read
your Supabase tables?

Your anon key already ships inside your app's JavaScript bundle. Anyone can read it out of the browser in about four seconds. The only thing standing between that key and your data is row level security, and in AI-built apps it is very often off.

Paste your own project URL and your own anon key below. This tool asks your project, as an unauthenticated visitor, which tables it will hand over. It reads nothing, changes nothing, and stores nothing.

  • Free, no account, no email required.
  • Read only. No write, no delete, no row data, no logging of your key or your table names.
  • Refuses a service_role key outright, in your browser, before it is sent anywhere.
Run the check

Two fields, one confirmation, about ten seconds.

Before you run it: since April 2026 Supabase only shows a project's table list to secret keys, which this tool will never accept. So it reads your table list when it can, and when it cannot it tests the names you paste above plus a built-in list of names AI builders commonly use. That can catch an exposed table, but it can never prove you have none, because a table with a name we did not test was not checked. Any non-exposed result is reported as inconclusive, never a pass, with three SQL queries that answer the question in full from your own dashboard.

Supabase dashboard, Project Settings, API. Only projects on supabase.co can be checked.

The public one, the one already in your frontend bundle. If you paste a service_role or secret key this page refuses it and never sends it.

Comma or line separated. Since Supabase stopped sharing table lists with the anon key, pasting your own names is the surest way to test the right tables. If you leave this blank we test a built-in list of names AI builders commonly create. Either way, tables with names we never test are not checked.

The button stays off until that box is ticked. The server checks for the same confirmation again and refuses the request without it.

~/supabase-exposure-check
idle

waiting for a project

ready
Paste your project URL and anon key, tick the confirmation, and the run log appears here.

Read only. No rows are read, stored or logged.

Check it by hand

Three queries, run in your own SQL editor.

The check above tests a list of table names, so it can catch an exposed table but it can never prove you have none: a table with a name it did not test was never checked. These three queries settle it. They run inside your own Supabase dashboard, see every table, view and policy regardless of its name, read only your database's catalog, and change nothing. Paste them into the SQL editor one at a time, so each result shows on its own.

01Tables with row level security switched off
-- 1. Tables in public with row level security switched off.
--    anon_can_read true = anyone holding your anon key can read
--    every row of that table.
select
  tablename as table_name,
  has_table_privilege('anon',
    format('%I.%I', schemaname, tablename), 'select')
    as anon_can_read,
  has_table_privilege('authenticated',
    format('%I.%I', schemaname, tablename), 'select')
    as logged_in_can_read
from pg_tables
where schemaname = 'public'
  and not rowsecurity
order by tablename;

Any row where anon_can_read is true is a table anyone holding your anon key can read in full. Where logged_in_can_read is true, so can anyone who can sign in.

02Policies that let every row through
-- 2. Policies that let every row through: using (true) or
--    with check (true). roles is who they apply to, and
--    {public} or {anon} means anyone at all.
select
  tablename as table_name,
  policyname as policy_name,
  cmd as applies_to,
  roles,
  qual as using_expression,
  with_check as check_expression
from pg_policies
where schemaname = 'public'
  and permissive = 'PERMISSIVE'
  and (
    regexp_replace(coalesce(qual, ''), '[\s()]', '', 'g')
      in ('true', '1=1')
    or regexp_replace(coalesce(with_check, ''), '[\s()]', '', 'g')
      in ('true', '1=1')
  )
order by tablename, policyname;

A permissive policy of using (true) or with check (true) waves every row through for whoever the roles column names. {public} or {anon} means anyone at all, {authenticated} means anyone who can sign up.

03Views that skip row level security
-- 3. Views anon can read that skip row level security. A view
--    without security_invoker reads its tables as its owner, so
--    their RLS never applies. A materialized view has no RLS.
select
  c.relname as view_name,
  case c.relkind when 'm' then 'materialized view'
    else 'view' end as kind
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and (
    c.relkind = 'm'
    or (
      c.relkind = 'v'
      and not coalesce(c.reloptions && array[
        'security_invoker=true', 'security_invoker=on',
        'security_invoker=1', 'security_invoker=yes'
      ], false)
    )
  )
  and has_table_privilege('anon', c.oid, 'select')
order by c.relname;

A view without security_invoker reads its tables as the view's owner, so the RLS on those tables never applies to it. A materialized view has no RLS at all.

An empty result from all three means none of these three problems is present. It does not cover storage buckets, database functions, auth settings, or anything in your app's own code.

What this is, and is not

A clean result here does not mean your app is secure.

We would rather undersell this tool than have you walk away from it believing something untrue. Here is exactly what it covers.

WHAT IT TESTS

One question, answered properly

  • Which tables your project hands over to an unauthenticated client: the ones it lists for your key, or, when it lists none, the names you paste plus a built-in list of common ones.
  • Whether an unauthenticated client holding your public key gets rows back from each table it tests.
  • Roughly how many rows are visible to that client, counted by your own database, never read by us.
WHAT IT DOES NOT TEST

What else can break an app

  • Whether a logged-in user can read another user's rows. That is a serious real flaw and it needs a real account to test.
  • Write and delete policies, storage buckets, edge functions, database functions, and auth configuration.
  • Secrets in your bundle or git history, your API routes, your dependencies, or your infrastructure.
  • Anything outside the public schema. This tool reads what your project exposes there and nothing else.
Read this before you quote the result anywhere

This is a self-check, not a security audit, and it is not an assessment against HIPAA, SOC 2, PCI DSS, GDPR or any other standard. It proves one thing if it finds something: an anonymous visitor could read that table. It proves almost nothing if it finds nothing, because a table that is empty today and a table that is properly protected look identical from the outside. No engineering firm, including ours, can certify you compliant with anything. We assess, harden, remediate, align systems to a standard's technical requirements, and prepare you for an audit someone else signs off.

What happens to your key

Nothing. That is the whole policy.

You are about to paste a credential into a stranger's form. You should be suspicious of that. Here is every single thing this tool does with it, and you can check most of it yourself in your browser's network tab.

  • It is only ever used to read from your project. Every request is an HTTP GET for your table list or an HTTP HEAD against a table. There is no write path, no delete path, no database function call, no storage call and no auth call anywhere in the code.
  • No row data reaches our servers. Table probes are HEAD requests. Whether a table is readable and how many rows are visible come from the status line and the Content-Range header your own database sets. Response bodies from your project are never read, so no record of yours is ever parsed, returned or stored by us.
  • Nothing about your project is persisted. Your key, URL, table names and result are never written to a database, a file or a cache. The result goes to your browser and is gone. Reload the page and it no longer exists anywhere. The one thing we keep is a rate-limit counter keyed on a salted one-way hash of your IP address and of the project reference, never the values themselves, and it expires within an hour.
  • Nothing identifying is logged. Our log line for a run is a timestamp, an outcome word, and four integers: tables found, tables probed, tables readable, milliseconds taken. Not your key, not your URL, not your project reference, not a table name, not a row count, not your IP address.
  • A privileged key is refused in your browser. A service_role key, an sb_secret_ key or a Postgres connection string is caught by the page itself and never sent to us at all. The server refuses the same shapes again if the page is bypassed.
  • Your key is cleared from the page when the run finishes. It is never written to localStorage, a cookie, or an analytics event.

Our servers do make the requests, which means our IP address appears in your project's logs, not yours. If you would rather nothing of ours touched your project at all, the honest answer is to skip this tool and run curl against your own REST endpoint, or ask us for the Rapid Diagnostic and give us scoped access on your terms.

If something came back readable

Do these three things today, in this order.

You do not need us for any of this. If you want it done properly, and the rest of the app checked while someone is in there, that is what the diagnostic is for.

01

Turn RLS on, then write a policy

Enabling row level security with no policy locks the table to everyone, including your own app. Enable it, then add the narrowest policy that lets a logged-in user reach only their own rows, and test the app immediately after.

02

Assume the exposed data is already out

If a table was readable, treat it as read. Work out what was in it, who it belongs to, and whether you have a notification obligation. That is a lawyer's call, not ours, and it is better made early.

03

Check the rest of the surface

A table open to anonymous readers is rarely the only thing. Logged-in users reading each other's rows, keys in the client bundle, and unauthenticated API routes travel together. Run through the security checklist.

Why we built this

This is not a rare mistake. It is the default outcome.

98%
of 1,072 scanned Supabase-backed vibe-coded apps had at least one security issue; 16% had critical flaws.
Symbiotic Security, 2026

An abstract statistic is easy to ignore

Reading that 98% of scanned apps had an issue changes nobody's Tuesday. Watching your own table names come back marked READABLE does. That is the only reason this tool exists: it turns someone else's research into your result, in ten seconds, for free.

See the full research page for every figure and its source. We cite real studies and never invent numbers.

Questions

Fair questions about pasting a key into a form.

Is it safe to paste my anon key here?

Your anon key is already public. It ships inside the JavaScript bundle your app sends to every visitor, so anyone who opens your site can read it. Pasting it here gives us nothing a visitor to your own app does not already have. What you must not paste is a service_role or secret key, and this page refuses those in your browser before they are ever sent.

What happens if I paste a service_role key by mistake?

The page detects it, refuses to send it, tells you to rotate it, and clears the field. A service_role key bypasses row level security completely, so anyone holding it can read, change and delete every row in your database. If you pasted one anywhere you should not have, rotate it in your Supabase dashboard now.

Do you store my results, my table names, or my key?

No. Your key, your URL, your table names and your results are never written to a database or a file. The result goes to your browser and is forgotten. The only thing kept is a rate-limit counter keyed on salted one-way hashes of your IP address and project reference, never the values themselves, and it expires within an hour. Our log line for a run records a timestamp, an outcome word, and four integers: tables found, tables probed, tables readable, and milliseconds taken. It does not record your key, your URL, your project reference, your table names, your row counts, or your IP address.

Can I use this on a project I do not own?

No. You have to confirm you own the project or are authorised to test it, and the check will not run without that confirmation. Testing someone else's system without permission is unlawful in most places. We rate limit this tool per visitor and per project, and it only works on projects hosted on supabase.co, so it cannot be pointed at anything else.

Nothing came back readable. Am I secure?

Not necessarily, and you should not treat this as a pass. The tool only tests what an unauthenticated visitor can read. It does not test whether a logged-in user can read another user's rows, which is a separate, serious flaw in AI-built apps that this free tool cannot see. It does not test write policies, storage buckets, edge functions, secrets in your bundle, your API routes or your dependencies. An empty table and a properly protected table look identical from the outside.

Why is this free?

Because it takes ten seconds and we would rather you found out from us than from an incident. If it finds something and you want the whole application checked properly by a senior engineer, that is the 1,500 dollar Rapid Diagnostic, and its fee is credited toward the full audit if you go ahead with one.

Found something? A senior engineer can tell you how bad it is.

The $1,500 Rapid Diagnostic puts a senior engineer in your codebase for a straight assessment of what is exposed and what it costs to close. The fee is credited toward the full audit.

Get a reply within one business day

Tell us what's broken.

A few lines is enough. Get a reply within one business day.

You get a reply within one business day. Prefer email? hello@rescueengineers.ai