RowShield
Guides

Is my Supabase secure? An honest answer, and how to find yours

Whether your Supabase project is secure depends on three things: Row Level Security enabled on every table PostgREST can reach, policies on those tables granting exactly what your product intends and nothing wider, and no credential that bypasses those policies sitting anywhere a browser can fetch. Everything else — backups, network restrictions, dashboards — matters, but those three decide whether strangers can read your data tonight.

This page turns each into a check you can run today, in order, with the queries inline. One honesty note up front: it is guidance rather than a product feature. RowShield automates the mechanical layer beneath these checks and says precisely where that layer ends; the judgment about what your policies should permit stays yours.

RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.

Check one: is every reachable table protected?

PostgREST exposes the tables it finds in the public schema, and the anon role holds broad grants there by design. So the first question is mechanical: which tables have Row Level Security switched on? The catalog answers directly:

SELECT n.nspname AS schema,
       c.relname AS table_name,
       c.relrowsecurity AS rls_enabled,
       c.relforcerowsecurity AS forced
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
  AND n.nspname = 'public'
ORDER BY c.relname;

Reading check one without false comfort

Any row with rls_enabled false is a finding — no context rescues a public-schema table while the anon key exists. But true proves less than it appears to: the flag says policies exist to consult, not what those policies grant. A constant-true policy passes this check while filtering nothing, and an enabled table with zero policies denies everyone instead, breaking the app quietly. That is why the first check alone settles neither direction.

Check two: what do the policies actually grant?

List every policy with its condition text and read it against what your product intends:

SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;

The three patterns that fail check two

Scan the output for the classic trio. A qual or with_check that is simply true grants every row to every targeted role. Write commands with an empty with_check accept rows attributed to anyone. Tables absent from the list entirely have no policies at all, which under enabled RLS means everything denied. Each failure gets fixed differently, which is why lumping them together as "an RLS problem" tends to produce the wrong patch.

Check three: where do bypass credentials live?

The strongest policies mean nothing to a caller holding a service_role key, because that role carries BYPASSRLS. Look wherever browsers can reach: bundled JavaScript, environment variables exposed to client builds, pasted snippets in tutorials you followed. The same outside-in request the probe makes — a GET with your anon key against each exposed table — closes the behavioural gap the catalog cannot see. Storage belongs in the same sweep: a public bucket publishes whatever it holds to anyone holding the URL.

Why the answer expires, and what keeps it fresh

A clean answer today is a statement about this afternoon. Migrations create tables, features widen policies, deploys move credentials into bundles — the three checks decay exactly as fast as the project ships. The dashboard Security Advisor is a good point-in-time tool and worth opening; it reports state at the moment you look, which leaves the interval between looks unwatched unless comparison runs on a schedule.

Marking scope precisely: RowShield does not automate the judgement in check two — no scanner decides what your policies ought to permit. What it automates continuously is the mechanical layer beneath all three checks: catalog rules covering RLS state, policy sanity, write clauses, indexes and storage; a probe testing what the anon key can actually read; alerts on transitions so silence means unchanged. RowShield reads pg_catalog and bucket metadata only, never rows, and is an independent product, not affiliated with or endorsed by Supabase. Run a free audit at rowshield.dev/audit to replace the feeling with evidence.

Frequently asked

Is Supabase secure by default?
The platform defaults are sensible, but application tables are only as secure as their RLS configuration: Postgres creates new tables with Row Level Security off, so an unprotected public-schema table stays world-readable until you enable it and add policies.
Does a green shield in the dashboard mean I am fine?
It means the advisor found nothing at the moment you opened it — configuration, checked once. The three checks above are behaviour plus configuration, and behaviour changes with every deploy, which is why scheduled comparison exists.
How often should I re-run these checks?
After every migration touching tables, and on a schedule regardless of deploys. The queries take minutes; the failure they catch takes months to notice otherwise, which is the entire economics of repeating them.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
is my supabase securesupabase security checkis supabase secure by defaultsupabase posture check