Elegant Tech
Guide · Supabase

Supabase RLS checklist

The question that matters before launch is simple: can user B see user A's data? In a Supabase app the answer depends almost entirely on Row Level Security. This checklist covers what to lock down, how to test it in 30 seconds and how to keep it tested in CI, based on the Supabase docs as of 2 November 2026.

Why the browser key is not the problem

Every Supabase app ships a key to the browser. Supabase describes the publishable key, and the legacy anon key it replaces, as safe to expose in a web page, a mobile app or source code. Anyone can read it, so it only reaches what Row Level Security allows.

That makes RLS the thing that actually protects your data. Once RLS is enabled on a table, no data is reachable through the API with the publishable key until you write policies. A table in an exposed schema with RLS turned off can be read by anyone who has the key, which means anyone who opened your app.

The secret key, and the legacy service_role key it replaces, is the opposite. Supabase states that a secret key bypasses every Row Level Security policy you have. It belongs only on servers you control, never in a browser, a shipped app or source control.

What to lock down, table by table

  • RLS on every table in an exposed schema. Supabase says to enable RLS on every table in an exposed schema, such as public. A table created in a hurry by a prompt is the one most likely to be missed.
  • Policies tied to the logged in user. Compare the row owner with auth.uid(), and name the role with to authenticated so the policy never runs for anonymous requests. For logged out requests auth.uid() returns null.
  • Wrap auth.uid() in a select. Write (select auth.uid()) = user_id instead of auth.uid() = user_id. Postgres then runs the function once per statement instead of once per row. Also add an index on every column your policies filter on.
  • with check on insert and update. The using clause decides which rows a user can read or change. The with check clause decides what the row may look like afterwards, which stops a user from writing a row with someone else's user_id.
  • Views. Views bypass RLS by default because they are usually created by the postgres user. On Postgres 15 and above, create them with security_invoker = true so they follow the policies of the tables underneath. On older versions, revoke access from the anon and authenticated roles.
  • security definer functions. These run as the role that created them, so they can read past your policies. Set search_path to an empty string, use schema qualified names inside, and never create one in an exposed schema.
  • Storage buckets. Access to files is controlled by RLS policies on the storage.objects table. Files in a public bucket are publicly accessible, so anything private, such as invoices or uploads, belongs in a private bucket with policies.
  • Secrets in the frontend. Search the built JavaScript for sb_secret_, and decode any legacy key you find to check that its role is not service_role. If a secret key was ever in the browser bundle or the repository, rotate it. Supabase rejects a secret key sent from a browser, but that does not stop someone using a copied key from other tools.

The 30 second self test

  1. Create 2 test accounts, user A and user B, and give user A a record, such as an order, a note or a project.
  2. Log in as user A and copy the id of that record from the URL or from the network request in developer tools.
  3. Log out, log in as user B, and open the same URL or replay the request with user A's id.
  4. If user B can see, change or delete user A's record, the app has a data leak. Fix the policy on that table before you do anything else.

Keep it tested: the Security Advisor and CI

The Security Advisor in the Supabase dashboard, under Advisors, runs fixed checks against your project. Run it before launch and after every schema change. The findings that matter most for this checklist are tables reachable through the API without RLS, views that run with elevated privileges, policies that always return true, and policies that call auth functions once per row. The same checks run from the CLI with supabase db advisors.

A manual test proves the app is safe today. A test in CI keeps it safe after the next prompt changes the schema. Supabase runs database tests written with pgTAP through supabase test db. Its testing guide shows helpers such as tests.authenticate_as to act as a given user, with results_eq to check that user A sees only user A's rows and throws_ok with error code 42501 to check that a blocked write is refused.

Supabase documents a GitHub Actions workflow that starts the local database with supabase db start and then runs supabase test db on every pull request. Add 1 test per table that holds user data, and a pull request that weakens a policy fails before it reaches production.

The 3 ways to production, and what each costs

  • Harden in place, from $5,000, 2 to 3 weeks. Lock down the database, fix auth, move secrets to the server, add monitoring. The frontend stays as it is. Right when you have under a few hundred users and the product still changes weekly.
  • Rebuild the backend, keep the front, $15,000 to $25,000, 6 to 8 weeks. New API layer, cleaned data model, real permissions, integrations moved server side, tests and CI. Right when users like the product but the plumbing cannot be trusted with money or personal data.
  • Full production rebuild, $25,000 to $35,000, 8 to 10 weeks. Same product decisions on a new stack, UX fixes from real usage, AI features designed in. Right when you are charging money, growing, and know exactly what version 2 should be.

These are our fixed prices for this work, with full IP transfer.

When this checklist is not enough

RLS protects rows reached through the Supabase API. It does not check logic in your own server functions, edge functions or webhooks that use the secret key, because those bypass RLS. If your app has several companies or workspaces sharing tables, roles inside a workspace or payments, test those paths separately, with 2 test accounts in 2 different workspaces.

Frequently asked questions

Yes, if it is the publishable key or the legacy anon key and RLS is enabled with policies on every table. Supabase describes that key as safe to expose. The secret key, or the legacy service_role key, must never be in the browser.

Supabase returns no data from that table through the API with the publishable key until you create policies. Enabling RLS first and then adding policies one at a time is the safe order.

No. Supabase states that a secret key, and the legacy service_role key, bypasses every Row Level Security policy. Use it only in server code you control.

Not by default. Views usually run as the postgres user and bypass RLS. On Postgres 15 and above, create the view with security_invoker = true so it follows the policies of its underlying tables.

Write pgTAP tests that act as a test user and check which rows that user can read and write, run them with supabase test db, and run that command in CI on every pull request.

Send us a test login. Get the check free.

We test whether one user can see another user's data and send a written report in 48 hours.

Get my free Launch Check ↗