Most small teams building on Postgres get access control the same way: the
application adds a WHERE user_id = ... clause to every query, and
everyone agrees to remember it. That works until somebody writes a new endpoint
at half past six on a Friday and forgets.
Row-Level Security moves the rule out of the application and into the database. Once a policy is in place, the database applies it to every query against that table, from every connection, whether the query came from your carefully reviewed data layer or from a one-off admin script somebody wrote in a hurry. The application can forget. The database will not.
This is the first question in the readiness guide's Data isolation section for a reason: in a multi-tenant product, it is the control that stops one customer's data reaching another. It is also one of the things no external scan can verify, which is why you have to go and look.
What RLS actually does
Enabling RLS on a table changes its default from "everyone with a connection can read everything" to "nobody can read anything." You then write policies that open it back up, row by row, according to rules you define.
A typical policy on a table of orders says something like: a row is visible
if its user_id column matches the ID of the authenticated user
making the request. The database evaluates that for every row, on every query.
A SELECT * FROM orders with no filter at all returns only the rows
that user is entitled to. Not because the application was careful, but because
the database refused to hand over the rest.
The important consequence: a missing WHERE clause stops being a
data breach and becomes an empty result set. That is a very large difference in
outcome for a very small difference in effort.
Checking it in Supabase
Supabase puts this front and centre, which is helpful. Log in, open your project, and go to the Table Editor. Each table in the list shows its RLS status. Tables with RLS disabled are flagged with a warning, and Supabase is fairly insistent about it.
Open the Policies tab, or go to Authentication and then Policies, to see the policies themselves. For each table holding customer data you want to see:
- RLS enabled, and
- at least one policy, and
- policies that reference the authenticated user rather than returning everything.
That third point is where the real checking happens. A policy of
USING (true) is a policy. It is also equivalent to having no
protection at all, because it matches every row. It is worth reading each
policy rather than counting them.
One Supabase-specific trap: tables created through the SQL editor rather than the dashboard do not get RLS enabled automatically. If your schema was set up with a migration file, check every table rather than assuming the default applied.
Checking it on RDS, Cloud SQL or self-hosted Postgres
There is no dashboard toggle. RLS is a Postgres feature, so you query the database for its state. Connected as a superuser or the table owner:
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;
Any row where rowsecurity is false has RLS off. To see the
policies themselves:
SELECT tablename, policyname, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
The qual column is the policy's actual condition. Read it. A
policy whose condition is true is decoration.
If you are on MySQL rather than Postgres, there is no direct equivalent. MySQL has views and column-level grants, which can be used to build something similar, but the enforcement is looser and the pattern is less common. Teams on MySQL usually rely on the application layer plus careful review, which is a legitimate answer as long as you can describe it when asked.
Turning it on without taking the site down
Enabling RLS on a live table with no policies denies access immediately. Everything that reads that table starts returning nothing. That is a genuine outage, and it is why this task often sits untouched.
The order that avoids it:
- Work on a copy first. Restore a recent backup to a staging database. You are going to break things, and you want to break them somewhere that does not matter.
- Write the policies before enabling RLS. Policies can exist on a table that does not yet enforce them. Add them, review them, then enable enforcement as the last step.
- Enable one table at a time, starting with the one that would hurt most in a breach rather than the one that is easiest.
- Exercise the application properly after each table. Sign in as a normal user and walk the main flows. Watch for empty lists where there should be data, which is what a too-strict policy looks like.
- Check your background jobs. Anything running on a schedule, anything processing a queue, anything sending a nightly email. These often connect with a different role and fail silently.
That last step catches the most people. Application flows get tested because somebody is looking at them. A cron job that quietly stops finding rows can run for weeks before anyone notices the emails stopped.
The service-role key bypasses all of it
This is the part that surprises teams who have just done the work. RLS
policies do not apply to a connection made with a service-role key, a
superuser, or any role with the BYPASSRLS attribute. That is by
design, because your migrations and admin tooling need to see everything.
It means your carefully written policies protect you exactly as far as that key stays out of reach. If the service-role key is in your frontend bundle, in a public repository, or in an environment variable on a machine more people can reach than you think, then RLS is not protecting anything. The database will hand over every row to whoever holds that key.
Enabling RLS and then leaving a service-role key in client-side JavaScript is a common and unfortunate combination. It looks like progress on a checklist and changes nothing in practice. Scoping those keys is the companion task, and neither is finished without the other.
What "done" looks like
You can answer yes to this guide item when:
- Every table holding customer data has RLS enabled.
- Each of those tables has at least one policy that references the
requesting user, not
true. - You have tested, with two accounts, that one cannot see the other's rows. That test is the subject of the next item in this section, and it is the only way to know the policies do what you think.
- Your service-role key is server-side only, and you know where it is used.
Notice that the last two are about verification rather than configuration. A policy that exists and a policy that works are different claims, and only one of them is worth putting in a security questionnaire.
If the answer is no
It is worth being unsentimental about this one. If you are running a multi-tenant product on Postgres without RLS, you are relying on every query ever written, by every person who has ever had commit access, being correct. That may well be true today. It is a claim that gets weaker every time someone new joins.
The work is usually a day or two for a small schema, most of it spent discovering which background jobs quietly depended on unrestricted reads. It is rarely the hardest thing on a readiness checklist, and it is close to the top for how much it reduces the blast radius of an ordinary mistake.