// docs / baas security / supabase rls scanner
Supabase RLS scanner: find tables with missing or broken row-level security
Row-level security (RLS) is the only thing standing between your customers' data and the internet when you ship a Supabase-backed app. AI coding tools generate RLS-shaped code that compiles, ships, and silently leaks data β tables created without RLS enabled, policies that read but never restrict, predicates that compare a column to itself. This article shows what a Supabase RLS scanner can prove from the outside, the four broken-RLS shapes that show up in vibe-coded apps, and how to scan your own deployment in under a minute.
What an external RLS scan can prove
A passive RLS scan looks at your Supabase project the way an anonymous visitor would. It uses only the publishable anon key β the same key your browser uses β and never authenticates as a user or touches service-role privileges. Anything it can see, an unauthenticated attacker on the internet can see.
From outside the database, a scanner can confirm the following with high confidence:
- RLS is disabled on a table. PostgREST returns rows for an anonymous
SELECTwhen RLS is off or when a policy permits it. Either case is a finding. - The anonymous role can list tables. A
GET /rest/v1/with the anon key returns the OpenAPI schema for every table that theanonrole has any privilege on. AI-generated apps frequently grantUSAGEon the schema andSELECTon every table, which exposes the full schema map even when RLS denies the actual reads. - Read access does not establish write safety. A passive scan cannot prove that INSERT, UPDATE, or DELETE policies protect every user and tenant. Review write policies and test them with isolated fixtures under the intended roles.
- The service-role key is in the browser bundle. Adjacent to RLS: if a scanner finds
SUPABASE_SERVICE_ROLE_KEYor any JWT withrole: service_rolein the JavaScript bundle, RLS is moot β the holder of that key bypasses every policy.
What an external scan cannot prove
Be honest about the scanner's boundaries. An external RLS scan cannot read your pg_policies table, your migration files, or the exact predicate of any policy. It infers from black-box behaviour, which means it will sometimes report a finding that turns out to be intentional public data (a marketing newsletter table, a public product catalog). The FixVibe report flags these as medium confidence when the scanner cannot disambiguate intent β review the table name and decide.
The four broken-RLS shapes AI tools produce
When you point Cursor, Claude Code, Lovable, or Bolt at Supabase, the same four broken-RLS patterns come up again and again. Each one passes type-check, compiles, and ships:
Shape 1: RLS never enabled
The most common failure mode. The migration creates the table but the developer (or the AI tool) forgets ALTER TABLE ... ENABLE ROW LEVEL SECURITY. PostgREST happily serves the entire table to anyone with the anon key. Fix: ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;, then add a policy per command. Enabling RLS with no policies denies all access through the API; the policies decide who gets in.
ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;Shape 2: RLS enabled, no policies
A more subtle failure. RLS is enabled but no policies are written. The default in PostgreSQL is deny, so authenticated users see nothing β and the developer adds USING (true) to make the app work, which permits everyone to read everything. Fix: write a policy that scopes by auth.uid(): CREATE POLICY "select_own" ON public.[name] FOR SELECT USING (auth.uid() = user_id); and a matching INSERT/UPDATE/DELETE policy.
CREATE POLICY "select_own"
ON public.[name]
FOR SELECT
USING (auth.uid() = user_id);Shape 3: Policy compares column to itself
A copy-paste artefact. The developer writes USING (user_id = user_id) β which is always true β instead of USING (auth.uid() = user_id). Type-checks pass; the policy permits every row. Fix: always compare a column to a function call (auth.uid(), auth.jwt()->>'org_id', etc.), never to itself or to a constant.
Shape 4: Overly permissive write policies
With RLS enabled, a role with no applicable policy is denied by default. A SELECT policy does not itself allow INSERT. Write exposure can arise from a permissive applicable INSERT or ALL policy, disabled RLS, or a role that bypasses RLS. Fix: inspect table privileges and every applicable policy, use ownership-scoped WITH CHECK conditions, and test writes with separate user identities.
How the FixVibe Supabase RLS scanner works
FixVibe's Supabase check works from the deployed app, in three steps:
- Find the project. FixVibe loads the deployed app and reads the Supabase project URL and public key from the same configuration your browser receives. No guessing, no brute force.
- See what is exposed. FixVibe checks which tables the anonymous role can see, without reading row data at this step.
- Stage 3 β inspect anonymous read access. The passive check reports observable access to discovered resources. It does not create, update, or delete database rows, and it cannot establish that all write policies or cross-user boundaries are correct.
Each finding ships with the exact request URL, response status, response shape (header-only), and the table name. FixVibe now labels whether the fix belongs in code/config, a provider console, DNS, secret rotation, or manual review so the next step matches who can actually apply it.
What to do when the scanner finds something
Every RLS finding is a runtime emergency. Public PostgREST endpoints get scanned by attackers in minutes. The remediation sequence is mechanical:
- Audit every table. Run
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';in the Supabase SQL editor. Any row withrowsecurity = falseis a problem. - Enable RLS on every public table. Default to
ENABLE ROW LEVEL SECURITYon every table created β make it a migration template. - Author policies command-by-command. Don't use
FOR ALL USING (true). Write explicit policies for SELECT, INSERT, UPDATE, DELETE β each one scoped toauth.uid()or an org-id column fromauth.jwt(). - Verify with a second account. Sign up as a different user, attempt to read another user's records via the REST API directly. If the response is
200, the policy is broken. - Re-scan. After applying the fix, re-run a FixVibe scan against the same URL. The Supabase RLS finding should clear.
-- Audit every table for missing RLS. Run in the Supabase SQL editor.
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;How this compares to other scanners
Most generic DAST tools (Burp Suite, OWASP ZAP, Nessus) do not know what PostgREST is. They will crawl your app, ignore the /rest/v1/ path, and report on the HTML pages they do understand. Snyk and Semgrep are static-analysis tools β they find migration files in your repo with missing RLS calls, but they cannot prove the deployed database is misconfigured. FixVibe sits in the gap: passive, BaaS-aware, focused on what an unauthenticated attacker can prove from the public URL.
Frequently asked questions
Will the scanner read or modify my data?
The passive RLS check is read-only. It examines observable anonymous read access and reports limited evidence. It does not perform database write operations or prove that every user and tenant is correctly isolated.
Does this work if my Supabase project is paused or on a custom domain?
Paused projects return 503 on every request β the scanner reports the project as unreachable. Custom domains work as long as the deployed app still loads the Supabase client SDK in the browser; the scanner extracts the project URL from the bundle either way.
What if my anon key is rotated or my publishable key changes?
Re-run the scan. The scanner re-extracts the key from the current bundle on every run. Rotation invalidates only the previous report, not the policy state of the database.
Does the scanner check the new Supabase publishable-key model (sb_publishable_*)?
Yes. The detector recognises both legacy anon JWTs and the newer sb_publishable_* keys and treats them identically β both are intended to be public and both leave RLS as the only line of defence.
Next steps
Run a free FixVibe scan against your production URL β the Supabase RLS check runs on every plan including the free tier. For a deeper read on what else can leak from a Supabase project, see Supabase service role key exposed in JavaScript and Supabase storage bucket security checklist. For the umbrella view across all BaaS providers, read BaaS misconfiguration scanner.
