Supabase RLS Is Enabled but No Policies Exist — What Actually Happens?

Enabling Row Level Security is an important step in securing a Supabase application, but enabling RLS is not the same thing as defining who should be able to access each row.

A common configuration looks like this:

alter table public.projects
enable row level security;

RLS is enabled, but no policy has been created.

This does not mean that every user can suddenly read the table. PostgreSQL does the opposite: once row security is enabled, normal row access must be allowed by an applicable policy. If no policy exists, PostgreSQL uses a default-deny policy, so rows are not visible and cannot normally be modified.

In a Supabase application, one common symptom is surprisingly simple: the rows exist in the database, but the client receives an empty result.

That makes this different from the more obvious failure of leaving an exposed table without RLS. RLS enabled with zero policies is usually an incomplete access-control configuration: the database is denying normal access, but the application still does not have the policies it needs.

RLS is an additional access-control layer

PostgreSQL privileges and RLS solve related but different problems. Traditional privileges such as GRANT SELECT determine whether a role can perform an operation on a table at all. RLS then determines which rows that role may access.

In Supabase, unauthenticated Data API requests normally use the anon PostgreSQL role, while signed-in requests normally use authenticated. Supabase recommends enabling RLS on tables in exposed schemas and granting only the privileges each role actually needs.

table privilege
    ↓
is SELECT / INSERT / UPDATE / DELETE allowed for this role?
    ↓
RLS policy
    ↓
which rows may this role access?

Enabling RLS without defining an applicable policy leaves the row-level layer closed.

What does “no policies” actually mean?

Case 1: the table has no RLS policies at all

This is the simplest configuration problem:

RLS enabled
+
zero policies

PostgreSQL applies default deny. For ordinary access subject to RLS, no rows are visible or modifiable until an applicable policy allows them.

Case 2: policies exist, but none apply to the request

This can produce very similar application behavior. A policy can be limited by PostgreSQL role, command such as SELECT or INSERT, its USING expression, or its WITH CHECK expression.

For example, a policy restricted to authenticated will not grant access to an unauthenticated request using the anon role.

That distinction matters during debugging: “there is a policy” is not the same as “there is an applicable policy.”

Why this often appears as an empty result in Supabase

Suppose public.projects contains rows, but your application query returns an empty array.

[]

If RLS is enabled, possible explanations include:

  • there is no policy,
  • the current role does not match the policy,
  • the authenticated user does not satisfy the policy expression,
  • or an application filter legitimately matches no rows.

Supabase specifically calls out enabled RLS with no matching policy as a common cause of empty results when rows exist.

A diagnostic query for tables with RLS but zero policies

PostgreSQL exposes enough catalog information to detect this condition directly. pg_class.relrowsecurity indicates whether row security is enabled for a relation, while pg_policy stores policies and links them to tables through the relation OID.

For the default Supabase application schema:

select
    n.nspname as schema_name,
    c.relname as table_name
from pg_class c
join pg_namespace n
    on n.oid = c.relnamespace
where c.relkind in ('r', 'p')
  and c.relrowsecurity
  and n.nspname = 'public'
  and not exists (
      select 1
      from pg_policy p
      where p.polrelid = c.oid
  )
order by
    n.nspname,
    c.relname;

If this returns tables, you have identified relations where RLS is enabled and PostgreSQL currently has zero policies defined for the table.

This query does not tell you what the policies should be. That requires understanding the application’s ownership, membership and access model. It answers one deterministic question: which application tables have RLS enabled but no policy definitions?

If your application exposes additional schemas, expand the schema filter deliberately rather than scanning PostgreSQL and Supabase internal schemas indiscriminately.

Inspect the policies that do exist

For tables that already have policies, PostgreSQL exposes the pg_policies view.

select
    schemaname,
    tablename,
    policyname,
    roles,
    cmd,
    qual,
    with_check
from pg_policies
where schemaname = 'public'
order by
    tablename,
    policyname;

This gives you a more useful review surface than simply checking whether the RLS toggle is on. It shows which roles and commands a policy targets and exposes the policy expressions that govern row visibility and row checks.

Do not trust a privileged database test as your only RLS test

One of the easiest ways to misdiagnose an RLS problem is to test with a privileged database role and assume that application users will see the same result.

PostgreSQL superusers and roles with BYPASSRLS bypass row security. Table owners also normally bypass RLS unless FORCE ROW LEVEL SECURITY is enabled.

A successful privileged query therefore does not prove that an anon or authenticated application request can see the same rows.

The service role is not an RLS user test

Supabase service-role and secret keys are intended for trusted server-side access and can bypass RLS. They should never be exposed in the browser.

That makes privileged server access useful for backend operations, but unsuitable as evidence that your client-facing RLS policies are correct.

ContextWhat you are testing
anonUnauthenticated application behavior
authenticated user AAllowed rows for one real user
authenticated user BUser or tenant isolation
Privileged/server roleBackend behavior, not client RLS correctness

Do not “fix” the problem by blindly adding USING (true)

Once you discover that RLS is blocking the application, a tempting fix is a policy with:

using (true)

That can be correct when broad row access is genuinely the requirement. Supabase documentation includes deliberately public policies that use true. The problem is using a broad predicate as an automatic debugging shortcut without first defining the intended access model.

The better question is not “How do I make this query work?” It is: Which actors should be allowed to perform which operations on which rows?

For a user-owned table, an intentionally scoped read policy might look like this:

create policy "Users can read their own profile"
on public.profiles
for select
to authenticated
using (
    (select auth.uid()) = user_id
);

This is only an illustrative ownership model. Your application may instead use organization membership, project membership, team roles, explicitly public rows or another documented authorization predicate. Do not copy an auth.uid() = user_id policy into a system whose ownership model is different.

A practical remediation sequence

1. Identify the actors

List the real authorization contexts: anonymous visitor, authenticated user, organization member, organization owner, trusted backend or other application-specific roles.

2. Identify the operations

Review SELECT, INSERT, UPDATE and DELETE separately. A user who may read a row does not automatically need permission to update or delete it.

3. Define the ownership or membership rule

Examples include a row owned by the current user, an organization the current user belongs to, an explicitly public row, or a documented administrative membership rule.

4. Create the smallest policies that implement that model

Prefer explicit roles and explicit command scope. Avoid broad access simply to make a failing query return data.

5. Review USING and WITH CHECK separately

Visibility of existing rows and permission to create or modify rows are not always governed by the same predicate. PostgreSQL distinguishes policy conditions used for existing rows from checks applied to new row values.

6. Test with real application authorization contexts

At minimum, test an unauthenticated request where relevant, an allowed authenticated user and a different authenticated user. For multi-tenant systems, explicitly verify that one tenant cannot access another tenant’s rows.

7. Retest the application path

A policy is successful when real application behavior matches the intended access model, not merely when a privileged SQL query starts returning data.

Pre-launch RLS checklist

  • RLS is enabled where required.
  • Every exposed application table has an intentional policy model.
  • Policies target the intended PostgreSQL roles.
  • SELECT, INSERT, UPDATE and DELETE have been considered separately.
  • USING expressions match the intended row-visibility rules.
  • WITH CHECK expressions match intended insert/update ownership rules.
  • Table privileges and RLS rules agree with each other.
  • anon behavior has been tested where relevant.
  • Authenticated behavior has been tested with more than one user where isolation matters.
  • A privileged query has not been used as the only test.
  • Broad true policies exist only where broad access is intentional.
  • Multi-tenant boundaries have been tested explicitly.

The important distinction

There are two different configuration problems:

RLS disabled on an exposed user-facing table

versus

RLS enabled but no policy exists

The first can remove row-level protection you expected. The second normally produces default-deny behavior for ordinary access subject to RLS. Neither should be treated as a finished authorization design.

The objective is not merely to switch RLS on. The objective is to create an explicit, testable access model in which each application role receives exactly the row access it needs.

Preparing a Supabase application for launch?

The Supabase Launch Audit reviews RLS enablement, policies, ownership predicates, multi-tenancy, schema design, keys, indexes and other visible production-readiness risks. It produces prioritized findings, evidence, remediation guidance and a readiness checklist. Starting at $499. It is not a security certification.

Sources and further reading