The Supabase RLS checklist
By Aakshar Garg, Co-founder, Fixier · 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.
Supabase now enforces part of this in the platform rather than leaving it to guidance: a secret key used from a browser is detected on the User-Agent header and always answered with HTTP 401 Unauthorized. Treat that as a backstop, not a safety net — it is one specific check, and it does not make the key safe to ship.
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 13-layer production check
Every audit we run walks the same thirteen layers, in this order. Each one is a question you can answer about your own app today — no tooling required.
- 1
Frontend foundationsDoes it break in front of the customer?
error boundaries on every route, image and bundle optimisation, a console that's clean on a live load
- 2
APIs & backend logicDoes your server trust whatever it's sent?
input validation on every endpoint, verified webhook signatures, idempotent writes, errors that don't leak stack traces
- 3
Database & storageCan a query be made to return someone else's rows?
parameterised queries, tenant isolation in the database, CVEs in the ORM and driver, a backup policy that exists
- 4
Auth & permissionsCan a stranger reach something they shouldn't?
the session checked on the server before data is returned, reset and verification flows, lockout, re-auth before destructive actions
- 5
Hosting & deploymentIf today's deploy is wrong, can you undo it?
a staging or preview environment, a written rollback path, env vars separated per environment, HTTPS and HSTS enforced
- 6
Cloud & computeWhat's the bill at a hundred times this traffic?
budget alerts, documented platform limits, a ceiling on AI and inference spend
- 7
CI/CD & version controlWhat stops a bad change reaching customers?
lint, type-check, test and build on every push, branch protection on main, an automated CVE gate, no pre-release packages in production
- 8
Security & RLSCan one customer see another customer's data?
row-level security, a scan for committed secrets, live penetration probing for injection and XSS, CSP and framing headers
- 9
Rate limitingCan one person hammer sign-in — or checkout — all night?
limits on sign-in and sign-up, on payment routes, and on every endpoint that writes
- 10
Caching & CDNCould one customer be served a page cached for another?
no-store on authenticated responses, cache keys that can't cross users, a revalidation strategy someone actually chose
- 11
Load balancing & scalingWhat happens when everyone arrives at once?
database connection pooling, a horizontal scaling path, a load test we run at the traffic you expect
- 12
Error tracking & logsWould you know it broke before a customer told you?
error tracking in the browser and on the server, structured logs with levels, alerts on the paths that matter
- 13
Availability & recoveryIf the data went wrong today, could you get it back?
a health endpoint, external uptime monitoring, an incident playbook, a restore you have actually run
Each layer in full — what it means, how to test it yourself, and the fix: the 13-layer production check.
Sources
Every claim above about a third-party tool was checked against that tool's own documentation on July 27, 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 thirteen 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