The Supabase RLS checklist
By Aakshar Garg, Founder, Scalable Systems · Last reviewed
the short answer
Row-level security decides which rows each user can touch. Supabase enables it by default for tables created in the Table Editor, but not for tables you create with raw SQL — and enabling it with no policies blocks everything instead of leaking. Your publishable key is safe in a browser precisely because RLS is doing the protecting, not the key.
Misconfigured row-level security is the mechanism behind almost every "why can this customer see that customer's data" incident in a Supabase-backed app. It's also completely learnable in an afternoon. This is the checklist we walk, in the order we walk it.
The seven RLS mistakes we actually find
The first and third are the ones that leak data. The second one is a bug, not a breach — worth knowing the difference so you don't panic-disable anything.
| What breaks | What you'd notice |
|---|---|
| A table created in SQL with RLS never enabledcritical | Every row is readable by anyone holding the publishable key — which is public by design. The Table Editor turns RLS on for you; the SQL editor does not. |
| RLS enabled with no policies at allmedium | The opposite failure: nothing leaks, but nothing works. Your app shows empty lists because no data is accessible until a policy exists. |
A policy that allows everyone — using (true)critical | Written to unblock development, never tightened. RLS is on, the dashboard looks correct, and every signed-in user reads every row. |
| A policy on SELECT onlyhigh | Reads are scoped to the owner but writes aren't, so a user can update or delete rows they can't even see. |
| The secret (service-role) key in client codecritical | That key carries the BYPASSRLS attribute — it skips every policy you wrote. Supabase says never to use it in a browser, even on localhost. |
| A view over a protected tablehigh | Views bypass RLS by default because they're usually created by the postgres user, so a view can expose exactly what the underlying table's policies forbid. |
| Policies that were never tested with two real usershigh | Everything looks right and nobody has ever confirmed that account B can't fetch account A's row by id. |
Is row-level security on by default in Supabase?
It depends how the table was created. Supabase's docs state that RLS is enabled by default on tables created with the Table Editor in the dashboard — but if you create a table with raw SQL, you have to remember to enable RLS yourself. That difference is where a lot of accidental exposure comes from, because AI builders often create schema via SQL.
Worth auditing every table, not just the recent ones: select relname, relrowsecurity from pg_class where relnamespace = 'public'::regnamespace; lists every table in your public schema with whether RLS is on.
Is it safe to expose the Supabase anon key?
Yes. Supabase documents the publishable (anon) key as safe to expose in a web page, mobile or desktop app, CI, or source code. What protects your data is Postgres row-level security on the anon and authenticated roles — not the secrecy of that key. Supabase is explicit that RLS must always be enabled on any table in an exposed schema.
The corollary matters as much: Supabase notes that publishable keys are not intended to protect against code analysis or browser inspection, because retrieving a key from a public component is always possible. If your security depends on nobody finding the key, you don't have security — you have obscurity.
Why does my app return an empty list after I enabled RLS?
Because enabling RLS without policies denies everything. Supabase's docs put it plainly: no data will be accessible via the API using a publishable key until you create policies. This is the safe failure mode — fix it by adding the policy you meant to add, never by turning RLS back off.
Do I need a separate policy for every operation?
Effectively yes, and they use different clauses. SELECT and DELETE policies use using; INSERT policies use with check; UPDATE uses both — using to decide which existing rows can be changed and with check to validate the new values. Supabase also notes that an UPDATE policy requires a SELECT policy to work.
The usual owner check is a comparison against auth.uid(), which returns the ID of the user making the request. A minimal per-user read policy is using (auth.uid() = user_id) — and the equivalent needs to exist for insert, update and delete before the table is actually scoped.
Does the service role key respect my policies?
No. It uses the Postgres BYPASSRLS attribute, which skips any and all row-level security policies you attach. Supabase's guidance is to never expose secret keys publicly — not in web pages, public documents, source code, or bundled into mobile, desktop or CLI apps, and never in a browser even on localhost.
Legitimate uses are server-side and developer-controlled: your own backend, edge functions, and admin tooling that does its own authorization checks before acting.
Do views respect RLS?
Not by default. Supabase documents that views bypass RLS because they are usually created with the postgres user. On Postgres 15 and later you can set security_invoker = true on the view so it obeys the policies of the underlying tables instead of the creator's privileges.
This is a genuinely easy one to miss, because the table it reads from is correctly protected. Inventory your views alongside your tables.
How do I test that RLS actually works?
Test as two real users, through your app's public key — never with the secret key, which bypasses everything and will make a broken setup look fine. Create a record as user A, then sign in as user B and try to read, update and delete it by id.
Do it for each of the four operations, not just reads, and repeat it for every table that holds customer data. Supabase's docs point to a community approach using pgTAP if you want these as automated database tests rather than a manual pass.
The seven-layer production check
Every audit we run walks the same seven layers, in this order. Each one is a question you can answer about your own app today — no tooling required.
- 1
AccessCan a stranger reach something they shouldn't?
authentication, session handling, ownership checks on every route
- 2
TenancyCan one customer see another customer's data?
row-level security, tenant scoping, IDOR on every id in a URL
- 3
SecretsWhat's shipped in the browser that shouldn't be?
keys in the client bundle, service-role credentials, token scope
- 4
Money pathsCan a customer be charged twice — or not at all?
idempotency, webhook retries, payment state reconciliation
- 5
Load and costWhat happens at a hundred times this traffic, and what's the bill?
N+1 queries, missing indexes, unbounded jobs, runaway spend
- 6
ObservabilityWould you know it broke before a customer told you?
error tracking, structured logs, alerting on the paths that matter
- 7
RecoveryIf the data went wrong today, could you get it back?
backups you've actually restored, reversible migrations, rollback
Sources
Every claim above about a third-party tool was checked against that tool's own documentation on July 26, 2026. These products change fast — if you spot something out of date, tell us.
Related guides
Want someone to actually check?
We audit AI-built apps against all seven layers and hand back a report in plain English — with the technical detail underneath for whoever fixes it. Startup-friendly pricing, scoped to your project.
Book an Audit