Row Level Security Mistakes in AI-Generated Supabase Apps

Written By
SprintX Team
AI & Product Engineering
July 29, 2026
6 min read

Eight Row Level Security failures we find repeatedly in AI-built Supabase apps, why the generator produces them, and the test that actually proves a policy works.
Supabase is the default backend for AI-built apps, and Row Level Security is what makes that safe. Configured correctly, the database itself refuses to hand a user rows that are not theirs, no matter what the client asks for.
The failure mode is nasty and specific: RLS produces no error when it is wrong in the permissive direction. A policy that lets everyone see everything behaves exactly like a correct one, right up until a second customer signs up and notices.
These are the mistakes we find over and over in AI-generated Supabase projects, roughly ordered by how often they cause a real incident.
1. The policy that permits everything
The most common finding by a distance. A policy exists, RLS is enabled, the dashboard is reassuringly green, and the expression is "using (true)" for the authenticated role.
The reason makes sense in the moment. RLS goes on, the app immediately returns empty arrays everywhere, the developer asks the AI to fix it, and the fastest thing that makes the error go away is a policy permitting everything. The app works again. Nobody revisits it.
Every "using (true)" policy on a table of user data is a full read of that table by any signed-in user — and signing up takes fifteen seconds.
2. RLS on the table, but the view is wide open
A table has correct policies. A view built on it does not enforce them, because by default a view runs with its owner's privileges rather than the caller's.
AI-generated apps love views: a dashboard summary, a joined listing, a convenience view with author details attached. Each one can be a bypass around the policies you carefully wrote. Views need to run as the invoking user, or need explicit protection of their own. Check every view in the exposed schema, not just tables.
3. A SELECT policy with no WITH CHECK on writes
USING decides which existing rows an operation may touch. WITH CHECK validates the row an insert or update proposes to create. Updates need both.
Generated policies frequently have USING and omit WITH CHECK, and the consequence is not subtle: a user can update a record they legitimately own and reassign its owner column to somebody else. The policy checks who is touching the row, never what the row becomes.
4. Roles read from client-editable metadata
A policy that reads a role from the JWT looks fine and is a full privilege escalation if it reads the wrong field. Supabase exposes user metadata the client can update and app metadata it cannot. Trusting the client-editable one lets any user make themselves an admin with a single SDK call.
Roles belong in a table the client cannot write to, or in app metadata set server-side only. Our multi-role RLS design guide walks through a hardened version.
5. The service key in the frontend
The service role key bypasses RLS entirely — that is its purpose, in trusted server contexts.
It ends up client-side when the AI hits a permissions error and reaches for the key that makes errors stop. Once it is in a browser bundle every policy in your project is decorative, and anyone who opens developer tools has it. Treat that as an active incident: rotate first, then fix the architecture, following what to do about API keys in the frontend.
6. Policies that recurse
A policy that calls a function which reads the same table triggers infinite recursion. At least this one is loud — it surfaces as a runtime error. The danger is in the fix: under time pressure, recursion gets resolved by disabling RLS rather than by moving the lookup to a separate, separately protected table.
7. Storage buckets nobody configured
Storage has its own policies, separate from your table policies. An app with immaculate table security frequently has a public bucket holding user uploads — invoices, ID documents, private images — reachable by URL with no authentication.
Long random-looking object URLs make this feel safe. It is not: URLs leak through logs, referrers, support tickets, and shared links.
8. Policies that make every query slow
A correct policy that calls a function per row will destroy performance at scale. Wrapping stable calls in a subselect lets Postgres evaluate them once per query, and policy filter columns need indexes like any others.
Not a security failure on its own — but it is why teams disable RLS under load, converting a performance problem into a security one.
| Symptom you see | Likely RLS cause | First check |
|---|---|---|
| Empty lists after enabling RLS | No policy for that operation | Which operations have policies |
| Everything works, for everyone | Policy is "using (true)" | Read every policy expression |
| Users can reassign records | Missing WITH CHECK on update | Update policies |
| A user became an admin | Role read from client-editable metadata | Where the role value comes from |
| Dashboard data leaks across tenants | Unprotected view | Views in the exposed schema |
| Files accessible without login | Public storage bucket | Bucket policies |
| Queries slow after adding policies | Per-row function calls, no index | Query plan on a policy-covered table |
How to test a policy properly
The dashboard SQL editor proves nothing about RLS. It runs privileged, so a query returning rows there says nothing about what a signed-in user can see. Neither does a script using the service key.
The only test that counts uses real user tokens:
- Create two ordinary test accounts in different tenants.
- Sign in as each through the normal client, so you hold their real access tokens.
- From A's session, try to read, update, insert, and delete B's records by ID, directly through the API rather than the UI.
- Confirm every one is denied and that A's own operations still work.
- Repeat unauthenticated, and as a user with no role row.
Write the results as a grid: operation by user, allow or deny. That grid is your specification, it belongs in the repository, and turning it into automated tests is straightforward — see adding tests to AI-generated code.
Repeat for storage buckets and edge functions — separate surfaces, separate rules — and check the auth bypass patterns that survive perfectly good RLS.
Frequently asked questions
My app broke the moment I enabled RLS. What is the right response? That breakage is correct behavior — the database is now denying what it was never told to allow. Write policies per operation, per table, rather than one blanket permission. Expect roughly an hour per table on a small app, and do it before launch; retrofitting policies onto live queries is much harder.
Is RLS enough on its own? It is the strongest single control available to you, because it sits below every code path. It also does not validate input, stop a stolen session, rate limit anything, or protect storage and edge functions. Treat it as the floor — the wider checklist covers the rest.
Can I just check policies in the dashboard? You can read them there, and you should. You cannot verify them there, because the editor bypasses RLS. Verification needs a real user token — which is exactly why so many broken policies survive review.
If nobody has ever tried to read another tenant's rows with a real user token, that test is the highest-value hour available to you. SprintX audits RLS table by table, rewrites the policies that only look safe, and leaves you a tested allow/deny grid you can run in CI. Send us your Supabase project.


