SUPABASE RLS IN THE WILD: 2026 MISCONFIGURATION ATLAS

Of 1,514 AI-built apps in our 2026 corpus, 891 (59%) shipped with at least one missing or broken Supabase Row Level Security policy. This atlas breaks the failure down by mode, by table name, and by platform — the first published quantitative cut of how vibe-coded apps actually misconfigure RLS.

Eighty-three percent of AI-built apps in our 2026 corpus use Supabase as their primary backend. Fifty-nine percent of those apps ship with at least one Row Level Security failure that lets an unauthenticated request read or write rows that should be protected. This is the first quantitative study we are aware of that breaks down which RLS failure modes occur, on which tables, on which platforms.

If you build on Supabase, the table below tells you which of your tables are statistically most likely to be exposed and which failure mode you are most likely in.

Headline numbers

Metric Value
Apps in corpus using Supabase 1,257 (83% of total)
Apps with at least one RLS failure 891
RLS failure rate among Supabase-backed apps 71%
Median exposed tables per affected app 3
Largest single-app exposure observed 41 tables
Window Nov 2025 – Apr 2026

Failure modes

We classify every RLS finding into one of five modes. The modes are mutually exclusive at the table level — one table is in exactly one mode.

Mode What it looks like in Supabase Apps affected Share of failures
RLS off “RLS Disabled” badge on the table 423 47%
Permissive policy RLS enabled, policy using (true) or similar 281 32%
Partial coverage SELECT locked, INSERT/UPDATE/DELETE open 128 14%
Service-role exposure Service-role key reachable from client bundle 41 5%
Misuse of auth.uid() Policy compares against wrong column or null-coalesces unsafely 18 2%

The first two modes account for 79% of all RLS findings. They are also the two failure modes that “RLS turned on” does not protect against — the dashboard shows a green checkbox in both cases.

Most-exposed table names

These are the table names most frequently found in a misconfigured state across the corpus. The naming gravity of AI generators is real — different builders converge on the same names, which makes these the highest-yield enumeration targets for an attacker.

Rank Table name Apps with this table exposed Share of exposed apps
1 users 312 35%
2 profiles 287 32%
3 invoices 156 18%
4 messages 142 16%
5 projects 128 14%
6 subscriptions 117 13%
7 comments 94 11%
8 orders 81 9%
9 tasks 79 9%
10 notifications 71 8%

users and profiles are the most common because Supabase Auth integrates with a profiles table by convention, and AI generators tend to mirror this with a separate users table for app-specific fields. When the policy on the auth-managed table is correct but the app-managed mirror is forgotten, both names show up exposed.

Per-platform breakdown

Platform Supabase-backed apps RLS failure rate Modal failure mode
Lovable 612 78% Permissive policy
Bolt.new 251 64% RLS off
Cursor 198 58% Partial coverage
Replit 142 67% RLS off
V0 (with Supabase backend) 54 41% Permissive policy

Lovable’s elevated rate has a structural cause: the platform’s generator adds tables incrementally as features are added, and the policy-creation step does not always run on the new table. We see clean Lovable apps regress to a permissive state within a single feature addition. The longitudinal study tracks this in detail.

CWE / OWASP mapping by failure mode

Each failure mode maps to a different CWE because the type of authorization gap is different. Triage and fix differ accordingly.

Failure mode CWE OWASP Fix shape
RLS off CWE-862 Missing Authorization API1:2023 BOLA · A01:2021 Enable RLS on the table; add a base policy
Permissive policy using (true) CWE-863 Incorrect Authorization API1:2023 BOLA · A01:2021 Replace with using (auth.uid() = owner_id)
Partial coverage (SELECT locked, write open) CWE-863 Incorrect Authorization API1:2023 / API3:2023 BOPLA Add INSERT, UPDATE, DELETE policies
Service-role exposure CWE-732 Incorrect Permission Assignment A05:2021 Security Misconfiguration · A02:2021 Rotate key; route writes through Edge Function with anon-role context
Misuse of auth.uid() CWE-863 Incorrect Authorization API1:2023 BOLA Compare against the correct column; never null-coalesce to a fallback

The CWE-862 / CWE-863 split is the practical line. CWE-862 is “no check exists at all” — easy to find, easy to fix. CWE-863 is “a check exists but is wrong” — harder to find because the policy looks like a policy, and the dashboard reports green.

Per-mode fix patterns

The fix per mode is short and mostly mechanical. The trap is mode-confusion — applying the RLS-off fix to a permissive-policy table leaves the table unchanged.

-- Mode 1: RLS off. Enable RLS, then add a base policy.
alter table invoices enable row level security;
create policy "owner_select" on invoices
  for select using (auth.uid() = owner_id);
-- Mode 2: Permissive policy. Drop the bad one, add the scoped one.
drop policy "anon_read_all" on invoices;  -- was: using (true)
create policy "owner_select" on invoices
  for select using (auth.uid() = owner_id);
-- Mode 3: Partial coverage. SELECT locked, but writes are open.
-- The policies for the other verbs are missing — add them.
create policy "owner_insert" on invoices
  for insert with check (auth.uid() = owner_id);
create policy "owner_update" on invoices
  for update using (auth.uid() = owner_id) with check (auth.uid() = owner_id);
create policy "owner_delete" on invoices
  for delete using (auth.uid() = owner_id);
-- Mode 5: Misuse of auth.uid(). Wrong column or unsafe coalesce.
-- WRONG: null auth.uid() is silently treated as a match
create policy bad on invoices
  for select using (coalesce(auth.uid(), invoices.owner_id) = invoices.owner_id);
-- RIGHT: compare against the correct column, return false on null
create policy good on invoices
  for select using (auth.uid() is not null and auth.uid() = owner_id);

For mode 4 (service-role exposure), the fix is structural: rotate the key in the Supabase dashboard, audit the bundle to confirm the new key is not present, and route any operation that genuinely needs service-role through a server-only Edge Function with the key sourced from the function’s secrets vault — not from the build env.

Why “RLS turned on” is not enough

The single most important number in this study is 32% — the share of failures where the dashboard says “RLS Enabled” but the policy does nothing. A founder reading the Supabase docs and turning on RLS gets the green checkbox, sees the badge in the dashboard, and assumes the work is done. The policy using (true) is what AI generators most often produce when asked to “add an RLS policy” without a specific access rule, because it satisfies the surface requirement without restricting anything.

If you only check whether RLS is on, you will miss a third of all real failures.

Methodology

Sample. All 1,257 Supabase-backed apps scanned by VibeEval between Nov 2025 and Apr 2026. Inclusion required confirmation of Supabase via DOM and network fingerprinting and builder consent for anonymized aggregation.

Probe. Each app’s anon key was extracted from the live JavaScript bundle. We enumerated tables via PostgREST OpenAPI introspection at /rest/v1/ and queried each table for unauthenticated reads. For tables that responded, we attempted unauthenticated writes (insert) to classify whether the failure was read-only or read-write.

Classification. Failure mode assignment is rule-based on the captured response code, response body, and PostgREST schema metadata. The full classification logic is reproducible by running the Supabase RLS checker against any URL.

Limits. This study measures what is reachable from outside with the anon key. It does not measure RLS misconfigurations behind the service-role key (which would require server-side access we do not have) or misconfigurations on tables that are present in the schema but not yet referenced by the OpenAPI spec.

Calibration against ref-rls. Every probe in the RLS suite is also run against ref-rls — a deliberately clean Supabase scenario where every table has the canonical using (auth.uid() = owner_id) policy on every verb. Any probe that fires on ref-rls is, by construction, a false positive — and the rule gets killed before it ships. The mode counts in the atlas above are net of false-positive elimination via the reference site.

Reproduce on the public benchmark

Each failure mode maps to a live scenario on gapbench.vibe-eval.com. The scenarios are running; the curl commands work today.

Scenario URL Failure mode reproduced
Supabase clone /site/supabase-clone/ All four primary modes co-located on one app
Indie SaaS /site/indie-saas/ Permissive policy + service-role JWT in bundle
Multi-tenant SaaS /site/multi-tenant-saas/ Partial coverage — reads locked, writes open across tenants
ref-rls (clean control) /site/ref-rls/ The “RLS done right” exemplar; same shape, no leak

For the anatomy of why the bug ends up shaped this way and the fix recipes per stack, see the companion pattern walkthrough: The Supabase service-role key in your frontend bundle.

How to reproduce a single data point

  1. Open the Supabase RLS Checker.
  2. Paste your live URL. The tool extracts your anon key from the bundle.
  3. The tool queries every public table and classifies the response.

A clean app reports zero exposed tables. An app with any non-zero count is in one of the five failure modes above.

Citations

VibeEval. Supabase RLS in the Wild: 2026 Misconfiguration Atlas. May 2026. https://vibe-eval.com/data-studies/supabase-rls-misconfiguration-atlas-2026/

RUN IT YOURSELF

Each scenario below is live on the public benchmark. The commands are copy-paste ready. Outputs may evolve as we tune the scenarios; the bug stays.

RLS off — Supabase clone scenario
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/users?select=*' -H 'apikey: ANON_KEY' -H 'Authorization: Bearer ANON_KEY'
expected 200 with the full users table — RLS not enabled, anon role unrestricted
Permissive policy — invoices visible to anon
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
expected 200 with every invoice — policy is using (true) on select
Service-role JWT in frontend bundle
curl -s https://gapbench.vibe-eval.com/site/supabase-clone/ | grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' | head -1
expected A JWT whose decoded role is 'service_role' — full bypass of RLS
Clean control — ref-rls returns nothing to anon
curl -s 'https://gapbench.vibe-eval.com/site/ref-rls/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
expected 200 with [] — RLS scoped on auth.uid(), anon sees no rows

COMMON QUESTIONS

01
Is RLS the same as authentication?
No. Authentication proves who a request is from. Authorization decides what they can read or write. Row Level Security is the authorization layer specifically at the row level — it lets you say 'this row belongs to user A and only user A can read it'. An app can have working authentication and still leak every user's data if RLS is missing.
Q&A
02
Why does Supabase ship the anon key to the browser?
Because Supabase is designed for client-side apps where the browser talks to the database directly through PostgREST. The anon key is a public credential by design. The actual security boundary is RLS — without it, the anon key gives full read/write access to every row in every public table. With correct RLS, the anon key only sees what the policies permit.
Q&A
03
What's the most common RLS failure mode?
RLS enabled with a permissive policy that effectively allows everything. We see policies like 'using (true)' on select operations, which means every authenticated user can read every row. The policy looks like a policy — Supabase shows it as 'enabled' in the dashboard — but it does not actually restrict anything. This is the failure mode that makes 'I have RLS turned on' a falsely reassuring answer.
Q&A
04
How do you test RLS without breaking my app?
By querying the public REST endpoint with the anon key. Supabase exposes every public table at https://<project>.supabase.co/rest/v1/<table>. A correctly configured table responds with empty results or a 401 to anonymous reads; a broken one responds with rows. The Supabase RLS checker queries a list of common table names this way and reports the diff.
Q&A
05
Why are some tables more often misconfigured than others?
Two reasons. First, tables added by AI generators after the initial scaffold often skip RLS — Lovable's generator does not consistently re-run policy creation when adding tables for new features. Second, certain table types have stronger naming gravity ('users', 'profiles', 'invoices') and AI generators converge on the same names, which makes them both more numerous and more visible to anyone enumerating common names.
Q&A
06
What is the fix for each failure mode?
RLS off: enable RLS on every table that holds user data. Permissive policy ('using true'): rewrite to constrain on auth.uid() = user_id or equivalent. Partial coverage (read-only locked, write open): add policies to insert, update, delete operations. Service-role exposure: rotate the key and remove from any client-reachable surface. The free RLS checker at the bottom of this page identifies which mode you are in for each affected table.
Q&A
07
What CWE numbers map to RLS misconfiguration?
CWE-284 (Improper Access Control) is the umbrella. CWE-862 (Missing Authorization) covers RLS-off. CWE-863 (Incorrect Authorization) covers permissive and partial-coverage modes. CWE-732 (Incorrect Permission Assignment for Critical Resource) covers service-role exposure. OWASP API1:2023 (BOLA) and API3:2023 (BOPLA) are the OWASP API mappings; OWASP A01:2021 Broken Access Control is the web-app mapping.
Q&A
08
Where can I see each failure mode on a real URL?
https://gapbench.vibe-eval.com/site/supabase-clone/ runs the full set: RLS off, permissive policy, partial coverage, service-role exposure all live. https://gapbench.vibe-eval.com/site/ref-rls/ is the clean control with policies correctly scoped on auth.uid() — same shape, no leak. Both are on our public benchmark, curl-reproducible.
Q&A
09
Why don't tests catch this?
Because most app test suites authenticate as a privileged user and assert the data comes back. RLS gaps require an unauthenticated probe — the bug only manifests when you query as the anon role. Few teams write tests that explicitly use the anon key against PostgREST, which is exactly what an attacker does first. The test you need is one line: query each table with the anon key and assert empty results.
Q&A

TEST YOUR SUPABASE RLS IN 10 SECONDS

Free RLS checker queries every public table from outside and reports which ones return rows without auth.

RUN RLS CHECKER