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
- Open the Supabase RLS Checker.
- Paste your live URL. The tool extracts your anon key from the bundle.
- 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/
Related
- Pattern walkthrough: The Supabase service-role key in your frontend bundle
- Pattern walkthrough: BOLA in AI-generated CRUD — the missing ownership check
- Pattern walkthrough: False positives and the ref0 control — the calibration methodology behind every detection
- Data study: 2026 AI App Security Benchmark — full corpus
- Data study: Where Vibe Coders Leak Their Keys — 2026 Frontend Secrets Report
- Data study: Broken Object-Level Auth in AI-Generated CRUD
- Data study: Honeypot Supabase: How long does a public anon key survive? — the time-to-abuse measurement
- Guide: Is My Lovable App Secure? Builder Checklist
- Tool: Free Supabase RLS Checker
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.
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/users?select=*' -H 'apikey: ANON_KEY' -H 'Authorization: Bearer ANON_KEY'
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
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
curl -s 'https://gapbench.vibe-eval.com/site/ref-rls/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
COMMON QUESTIONS
TEST YOUR SUPABASE RLS IN 10 SECONDS
Free RLS checker queries every public table from outside and reports which ones return rows without auth.