My Supabase RLS Policy Is Not Working

Written By
SprintX Team
AI & Product Engineering
August 07, 2026
6 min read

A debugging order for Supabase row-level security when policies seem ignored or block everything, including the service-role trap and the tests that prove it.
Two versions of this problem arrive at our inbox. In the first, you wrote a policy, the app still returns every row to every user, and it feels like the policy is being ignored. In the second, you wrote a policy and now nothing comes back at all, for anyone, including you.
Both usually have the same root: RLS is doing exactly what it was told, and the thing you are testing with is not the thing your policy applies to.
Work through the checks in this order. The first three explain the large majority of cases and take two minutes each.
Check 1: are you querying as the service role?
This is the most common cause of "my policy does nothing" by a distance.
Supabase gives you two keys. The anon key represents an untrusted client and is fully subject to row-level security. The service role key bypasses RLS entirely, by design — it exists for trusted server-side work.
If your app initializes the Supabase client with the service role key, every policy you write is decoration. Queries return everything. The policy is fine; it is simply not being consulted.
AI-generated code reaches for the service role key constantly, because it makes queries succeed and the model is optimizing for a working call. Check every client initialization in your codebase, not just the main one. Then check whether that key ever reached the browser bundle — if it did, treat it as leaked and read what to do when an API key is exposed before anything else, because a public service role key means your entire database is readable by anyone who opens DevTools.
The correct split: browser and untrusted contexts use the anon key with RLS enforced; the service role key lives only in server-side code, in environment variables, never in a client component.
Check 2: is RLS actually enabled on the table?
Writing a policy does not turn RLS on. They are two separate operations, and this trips up people who create policies through the dashboard's SQL editor.
A table with policies but with RLS disabled behaves as if the policies do not exist. Supabase shows a warning for this in the table editor, but it is easy to miss when you created the table with a generated migration.
Two related traps: every table you add later starts with RLS off, so an audit table or an uploads table created after your initial security pass is wide open. And views do not inherit policies the way you expect — a view over a protected table can expose rows depending on how it is defined, so audit views separately.
Check 3: what does the policy actually compare?
If RLS is on and you are using the anon key, then the policy is being evaluated and the result is what it says. Read it literally.
The typical mistakes:
- Comparing the wrong identity. The policy checks a user ID column against the authenticated user's ID, but your rows store an email, an organization ID, or a profile ID instead. Different column, no match, zero rows.
- Type mismatch. The auth user ID is a UUID. If your column is text, the comparison may fail silently depending on how it is written.
- No policy for the operation you are performing. Policies are per operation. A select policy does not permit an insert. Locked-out writes with working reads are almost always this.
- Insert policies need the right clause. An insert is checked against what the new row will contain, not against existing rows — a policy written only as a using clause will not let anything in.
- Permissive versus restrictive. Multiple permissive policies on the same operation combine with OR. Adding a second policy to tighten access frequently loosens it instead.
Symptom to cause, at a glance
| What you observe | Most likely cause |
|---|---|
| Every user sees all rows | Service role key in the client |
| Policies exist but do nothing | RLS never enabled on the table |
| Reads work, writes fail | Missing insert or update policy |
| Nothing returns for anyone | Identity column mismatch, or no matching policy |
| Works in the SQL editor, fails in the app | SQL editor runs as an admin role |
| Works for the owner, fails for teammates | Policy checks ownership, not membership |
| Related data leaks through a join | The joined table has no policy of its own |
That last row deserves emphasis. Protecting a table protects that table. If your orders table is locked down but your line items table is not, anyone who can query line items can reconstruct the orders. Every table containing tenant data needs its own policy, including the join tables nobody thinks about.
Prove it with two accounts, not with reasoning
The only test that means anything is running the real query as two different real users. Create user A and user B in your auth table, give each of them a record, then, using the anon key and a genuine session for A, request B's record by ID.
You are looking for zero rows. Not an error, not a filtered UI — zero rows returned by the database.
Repeat that for select, insert, update, and delete, on every table with tenant data — about twenty minutes for a typical app. Then write it down as a test so it stays true; a policy that was correct in June and broken in August is the standard outcome when nobody is checking. The workflow for that lives in adding tests to AI-generated code.
The dashboard's policy editor and the SQL editor both execute with elevated privileges, which is why a query that returns the right thing there can still leak in your app. Never accept the SQL editor as proof.
The failure mode behind the failure mode
Stepping back: RLS problems in AI-built apps are rarely about a single policy. They are about the app never having had an authorization model. The generated code filters by user ID in the frontend query, that looks correct in the UI, and the database happily serves anything to anyone who calls it directly.
Escape.tech found more than 2,000 vulnerabilities across 5,600 vibe-coded apps, and this class — data access controlled in the client rather than the database — is one of the most frequent. If you are unsure how deep it goes in your app, the patterns are catalogued in Supabase RLS mistakes in AI apps, and the conceptual grounding is in our guide to row-level security and roles.
Two habits keep it fixed. Deny by default: enable RLS on every table at creation and add access deliberately. And put policies in migrations rather than clicking them into the dashboard, so they are versioned and identical between environments.
Frequently asked questions
Is the anon key safe to ship in my frontend? Yes — that is what it is for. It identifies your project, not a privileged user, and every request made with it is subject to your policies. Its safety depends entirely on those policies existing.
Why does my query work in the Supabase dashboard but not in my app? The dashboard's SQL editor runs with a role that bypasses RLS. Your app runs as the anon or authenticated role. Always verify with an actual client session, not the editor.
Do I still need server-side checks if RLS is correct? For data access, well-written policies are the strongest layer you have, because they hold even if someone calls the API directly. You still need server-side logic for business rules RLS cannot express — spend limits, workflow states, and anything requiring multiple tables to agree.
If your policies are ignored, over-strict, or you simply cannot tell which, the underlying issue is usually an authorization model that was never designed. SprintX audits Supabase-backed apps table by table, fixes the policies in migrations, and leaves you with tests that prove they hold. Send us your project and we will run the two-account test.


