RowShield
Guides

The Supabase security checklist, complete and current

This is the complete checklist for securing a Supabase backend: twenty-one checks across six areas, each stated plainly, each with the query or command that verifies it. It is written to be worked through once and revisited forever, because every item on it decays the day after the next migration lands.

Two honesty notes first. The checklist is maintained by hand: RowShield automates much of the mechanical layer below, and the closing section says exactly which parts, but the checklist as such is guidance rather than a scan. And it is engineering hygiene, not legal or compliance advice — it produces evidence a team can read, never a certification.

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.

Tables and policies (checks 1–6)

Check 1 — Row Level Security is enabled on every table in the public schema, including the ones added since you last looked. Check 2 — FORCE is set alongside enabled, because migrations and psql sessions run as the owner and the owner otherwise bypasses everything. Check 3 — every policy scopes to the column that genuinely expresses ownership in your schema, not to a placeholder copied from documentation. Check 4 — no permissive policy evaluates a constant true, the form that filters nothing while looking configured. Check 5 — every INSERT, UPDATE and ALL policy carries a WITH CHECK, so writes are attributed to their true authors. Check 6 — any table with RLS enabled and zero policies is a documented, deliberate lock-out rather than unfinished work.

Two queries cover checks 1 through 4 mechanically; the remaining two are a read of the policy list against intent:

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;

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

Credentials and keys (checks 7–10)

Check 7 — the anon key is treated as public and every read it can perform is one you would publish, because it ships in every browser bundle by design. Check 8 — service_role and any secret-bearing key exist only on server paths: routes, edge functions, schedulers, never client code. Check 9 — any privileged key that ever reached a bundle has been rotated, not merely deleted from source, since copies persist in caches, logs and history. Check 10 — the newer shapes receive the same discipline: sb_secret_ keys and sbp_ management tokens stay server-side even when the legacy JWT is gone.

The behavioural version of checks 8 and 9 is the same request a stranger would make — fetch your own deployed bundle and search it:

curl -s "https://your-app.example.com" | grep -oE "sb_(secret|publishable)_[A-Za-z0-9]+|eyJhbGci[A-Za-z0-9._-]+"

Storage (checks 11–13)

Check 11 — buckets are private unless they genuinely publish, because a public bucket serves its contents to anyone holding or guessing object URLs. Check 12 — public buckets contain only what marketing would approve of, verified by listing rather than memory. Check 13 — the storage.objects table itself runs with RLS enabled and policies matching your intent, since object-path guessability converts a loose policy into direct reads.

SELECT name, public, created FROM storage.buckets ORDER BY name;

Performance under policy load (checks 14–16)

Check 14 — every column referenced in a policy predicate has a valid index, because an unindexed predicate degrades each query toward a sequential scan and is the common reason Supabase bills outrun traffic. Check 15 — auth.uid() calls are wrapped as (SELECT auth.uid()), evaluated once per statement with identical semantics. Check 16 — EXPLAIN (ANALYZE) runs after meaningful policy changes, reading plans rather than trusting vibes.

Checks 14 and 15 share one preparatory query — list policies and indexes, then compare the columns mentioned in each:

SELECT p.tablename, p.policyname, p.qual, p.with_check
FROM pg_policies p WHERE p.schemaname = 'public';

SELECT tablename, indexname, indexdef
FROM pg_indexes WHERE schemaname = 'public'
ORDER BY tablename;

Process and drift (checks 17–19)

Check 17 — the section-one queries rerun after every migration touching tables, because the failure re-enters exactly the way it entered: another generated table, another missing pair of lines. Check 18 — pull-request descriptions name policy changes explicitly, giving review a place to look and a reason to ask. Check 19 — comparison runs on a schedule rather than on memory, pairing the dashboard Security Advisor, which is a good point-in-time tool worth opening, with scheduled external checks for the interval between openings. The pairing matters more than either half alone: the advisor examines deeply when asked, the schedule asks whether anything changed while nobody was looking, and drift lives entirely in that between-time.

Automating the checklist, honestly (checks 20–21)

Check 20 — know precisely what automation covers. RowShield runs the nine shipped rules on every scan: RLS_DISABLED, RLS_NO_POLICIES, RLS_TAUTOLOGY, MISSING_WITH_CHECK, UNINDEXED_RLS_PREDICATE, RLS_UNWRAPPED_AUTH_CALL and PUBLIC_BUCKET_EXPOSURE through catalog reads, SERVICE_ROLE_KEY_EXPOSED and ANON_TABLE_READABLE through the outside probe — with alerts on transitions, generated remediation SQL from your real columns, daily scans free, and CLI exit codes for CI gates. Check 21 — keep the rest human: RowShield does not automate judgement about what your policies should permit, application-screen testing, dependency scanning or compliance mapping, and this page is hand-maintained guidance rather than a scan result. The division is the point: machinery holds the floor steady so your attention goes to the decisions only a person can make.

Cadence follows the plans — Free daily, Indie hourly, Team every fifteen minutes with Discord and custom webhooks, Growth quoted above that — with rates on the pricing page, which takes an email until checkout opens. RowShield reads pg_catalog and bucket metadata only, never rows, and is an independent product, unaffiliated with and not endorsed by Supabase. Start with the free audit at rowshield.dev/audit — a URL is the whole input.

Frequently asked

How long does the checklist take?
The first pass on a modest project is measured in hours, most of it spent writing owner-scoped policies rather than running queries. Subsequent passes shrink to minutes because only the deltas need attention.
Does completing this checklist satisfy an auditor?
No — it is engineering hygiene, not legal or compliance advice. Auditors ask for evidence produced over time, which is a different artifact from a good afternoon, though the checklist generates exactly the raw material such evidence is built from.
Which checks should I automate first?
The mechanical ones: RLS state, policy presence and sanity, write-clause coverage, storage posture and the anonymous read test. They are fully specified by queries, they decay fastest, and they are precisely the nine rules RowShield ships.
Does the checklist change for client projects?
The checks are identical; the workflow differs, since rosters multiply the remembering. The agency workflow page covers routing alerts per client and producing evidence you can forward.

Check your project in about ten seconds

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

Run the free audit
supabase security checklistsupabase hardening checklistsupabase security best practicessupabase production checklist