Supabase RLS explained: policies, grants and how to test that they hold

Supabase RLS is the Postgres feature that decides which rows a signed-out visitor, a signed-in user and your own server can read or write through the API. Switch it on, give each role the grants it needs and write one policy per operation. Then test every table as each of those three before anyone else does.

Supabase security for AI-built apps

RLS on, no policy
Zero rows through the API
Secret key
Skips RLS, servers only
Views
Bypass RLS by default
Default change
30 October 2026
Checked
28 October 2026

The short answer

Supabase RLS is Postgres row level security. With RLS on and no policy, the anon and authenticated roles can read and write nothing, and each policy then opens named rows to named roles. It sits beside grants, not above them. Views can bypass it, the secret key skips it, and Supabase's 30 October 2026 default change only reaches new tables.

What Supabase RLS does in three states

RLS is a switch on each table plus the policies that go with it. The table below is the whole model. We did not quote it, we ran it, in a throwaway Postgres 18 database that uses the same three roles Supabase does (the test is further down). Alice owns two of the three rows.

Rows each role gets back from a select on a table of three rows
State of the tableanon (signed out)authenticated (Alice)service role
RLS off, default grants3 rows3 rows3 rows
RLS on, no policy0 rows0 rows3 rows
RLS on, policy: own rows for authenticated0 rows2 rows3 rows
Same policy, read through a view with default settings3 rows3 rowsNot run
Same policy, read through a view with security_invoker on0 rows2 rowsNot run

Grants and policies are two separate checks

Postgres runs two checks before a request touches a table.

Grants decide whether a role can run an operation on the table at all. Policies decide which rows that operation reaches. Supabase's own guide warns that a table in an exposed schema without RLS is readable and writable by any role that holds a grant, and that adding policies does not take the grants back. On existing projects a new table in the public schema starts with every privilege granted to anon, authenticated and the service role. The grant check passes for everyone, which leaves your policies as the only thing between the open internet and your rows.

The 30 October 2026 default change, and what it leaves alone

  • New tables only. From 30 October 2026 Supabase applies its default of not exposing new tables to the Data API to existing projects. A table you create after the change needs an explicit grant before the API can see it.
  • Old tables keep their grants. Supabase says existing tables are not affected, so every table created before the change stays reachable. Review those yourself with the Security Advisor, and revoke the grants you do not need.
  • RLS works as before. Grants are a separate layer from policies, and Supabase says enabling RLS and writing policies stays the same.
  • Direct connections are unaffected. The change touches the Data API, the REST and GraphQL layer that supabase-js calls, and does not touch an app that talks to Postgres with a connection string.

The dates come from Supabase's announcement of 28 April 2026, which also puts the default for all new projects at 30 May 2026. You can adopt it today. The announcement shows the revoke statements, and the setting is called Automatically expose new tables in the Data API settings.

One table, four policies

Write one policy per operation. Supabase's guide recommends it because a policy for all operations hides which operation each rule was meant to cover. This is the pattern for a table where each user owns their rows, following the guide's own example. Each statement ends with a semicolon when you run it, left off in the listing.

alter table public.todos enable row level security
revoke all on table public.todos from anon, authenticated
grant select, insert, update, delete on table public.todos to authenticated

create policy "read own" on public.todos for select to authenticated
  using ( (select auth.uid()) = user_id )

create policy "insert own" on public.todos for insert to authenticated
  with check ( (select auth.uid()) = user_id )

create policy "update own" on public.todos for update to authenticated
  using ( (select auth.uid()) = user_id )
  with check ( (select auth.uid()) = user_id )

create policy "delete own" on public.todos for delete to authenticated
  using ( (select auth.uid()) = user_id )

Four details carry most of the weight. Wrapping auth.uid() in a select lets Postgres evaluate it once per query, and Supabase's advisor has a lint, 0003, for the missing wrapper. An update needs a matching select policy to work at all. The with check clause on update stops a user reassigning user_id to a different owner. And the column a policy filters on needs an index, because Postgres tests the policy against every candidate row.

Mistakes that leak data, and the fix for each

Supabase ships advisor lints for most of these, and they are the cheapest check you can run. One public example is CVE-2025-48757, a record from May 2025 about insufficient RLS policies in sites generated by Lovable, which Lovable disputes because each customer is responsible for their own app's data. Whatever the outcome of that dispute, the mechanics are the ones in this table, and the Lovable security audit page covers what to do about them.

Six RLS mistakes, what each one does and how to fix it
MistakeWhat happensFix
RLS never switched on for a table in publicAnyone with the project URL can read, edit and delete every row. Lint 0013 calls it critically unsafeEnable RLS, then write policies. Until you do, anon gets nothing
A policy that says using (true)Every row opens to the roles listed. Lint 0024 flags always-true policiesReplace it with an owner or membership check, or limit it to authenticated
A policy that trusts user_metadataUsers can edit their own metadata, so a policy that checks an is_admin flag there lets them promote themselves. Lint 0015Keep roles in a table users cannot write to, and check that table
A view over a protected tableViews bypass RLS by default, because they are created by the postgres user. In our test the view returned all three rows to a signed-out visitor. Lint 0010On Postgres 15 and above create the view with security_invoker on, or keep it in an unexposed schema
Default grants never revokedPolicies do not remove grants. The defaults include truncate, references and trigger, which the Data API does not use but SQL running as anon or authenticated, such as a function, canRevoke all from anon and authenticated, then grant back only what each role needs
The secret key in browser codeIt authorizes as the service role, which has bypassrls, so every policy is skippedPublishable key in the browser, secret key on servers only, and rotate any secret key that has been exposed

How to test a policy before you trust it

  1. Write the expected result first. For every table list what anon, a signed-in owner, a signed-in stranger and the service role should get from select, insert, update and delete.
  2. Run each case as that role. In the SQL editor start with set local role anon and then the query. For a signed-in user also set the subject claim, as Supabase's own test files do with set local request.jwt.claim.sub.
  3. Automate it. Supabase's guide asks for one pgTAP test file per table under supabase/tests and runs it with supabase test db. A missing grant raises error 42501 before any policy runs, so throws_ok covers the deny cases and results_eq covers the allow cases.
  4. Run the Security Advisor. Lints 0007, 0008, 0013, 0015 and 0024 catch a policy on a table with RLS off, RLS on with no policy, RLS off in public, user_metadata inside a policy and an always-true policy.
  5. Run the whole set again after every migration. A new table is a new chance to forget the first line of this page.

What we tested on 28 October 2026

We rebuilt the three states in a throwaway database that holds nothing from any real project. It runs on PGlite 0.5.13, which is Postgres 18.6 compiled to WebAssembly, so the RLS behavior is Postgres's own. We created the roles anon, authenticated and service_role (with bypassrls), an auth.uid() function that reads the subject claim, and a todos table with three rows, two of them Alice's. Every line below is a select run under set role.

1. RLS off, default grants
  anon                       3 rows
  authenticated (alice)      3 rows
  service_role               3 rows
2. RLS on, no policy
  anon                       0 rows
  authenticated (alice)      0 rows
  service_role               3 rows
3. RLS on, owner policy for authenticated
  anon                       0 rows
  authenticated (alice)      2 rows
  service_role               3 rows
4. view over the protected table, default owner
  authenticated (alice) via view   3 rows
  anon via view                    3 rows
5. same view with security_invoker = true
  authenticated (alice) via view   2 rows
  anon via view                    0 rows
6. grant revoked from anon
  anon                       ERROR 42501 permission denied for table todos

The view is the line to remember. With RLS on and a correct policy, a view with default settings still handed all three rows to a signed-out visitor, and one setting fixed it. A plain Postgres database is not a Supabase project, so the Data API, the dashboard and the key system are outside this test. What it shows is that this behavior belongs to Postgres and not to Supabase.

Where RLS stops

Secret keys. The secret key and the legacy service_role key skip RLS, so they stay on servers and out of browser code. Supabase is deprecating the legacy anon and service_role keys by the end of 2026.

Storage. Files sit behind RLS policies on storage.objects, and by default Storage allows no uploads to a bucket without one. A loose policy there opens files the way a loose table policy opens rows.

Functions. New functions in the public schema are callable by every role unless you revoke execute. Lint 0028 flags security definer functions that anon can run.

Abuse. RLS limits which rows a request reaches and does not limit how often one arrives. Supabase's guide treats rate limits, extra API keys and quotas as pre-request checks, a separate control.

Where this sits in a production review

  • Production engineering. A production review will give you a scorecard across ten layers with evidence for each, and database access is one of the ten.
  • AI agent development. An agent that writes SQL or calls your API needs the grants and policies above before it touches real data.
  • Custom AI development. A platform built on Supabase gets its policies, tests and migrations designed in from the first table.

Two sibling guides cover other places an AI-built app leaks: MCP security best practices and AI red teaming tools. If your secrets sat in environment variables on a host that was breached, the Railway incident brief lists what to rotate, a database password and a Supabase key among them. If a tool wrote your Supabase schema, the vibe coded app audit covers access, keys and deploys in one pass.

Frequently asked questions

What is Supabase RLS?

Supabase RLS is Postgres row level security as used by Supabase's API. You switch it on per table and write policies that say which rows each role can select, insert, update or delete. With RLS on and no policy, the anon and authenticated roles get no rows. The service role and the table owner skip it.

Why does my Supabase query return no rows after I enable RLS?

Postgres applies a default-deny rule when a table has RLS on and no policy, so nothing is visible or writable through the API. Add a policy for the role you query as, such as a select policy for authenticated that checks auth.uid() against the owner column. If you get a permission error instead of an empty result, the role is missing a grant.

Is it safe to put the Supabase anon or publishable key in my frontend?

Yes for the publishable key, and for the legacy anon key it replaces, provided every exposed table has RLS and policies. Supabase describes the publishable key as safe to expose because it only reaches what RLS allows. The secret key and the legacy service_role key bypass RLS and belong on servers only.

Does the service role key bypass RLS?

Yes. The secret key authorizes as the service_role Postgres role, which has the bypassrls attribute. Supabase notes one exception. If the request also carries a signed-in user's access token, it runs under that user's policies, even when the client was created with a secret key.

Do Supabase views respect RLS?

Not by default. A view created by the postgres user bypasses the policies on the tables beneath it, and we saw a signed-out visitor read all three rows through one. On Postgres 15 and above, create the view with security_invoker on. On older versions keep it out of the exposed schemas.

What changes for my Supabase project on 30 October 2026?

Supabase applies its default of not exposing new tables to the Data API to existing projects. New tables in the public schema then need an explicit grant before the API can reach them. Existing tables keep their grants, RLS and policies work as before, and apps that use a direct Postgres connection are not affected.

Sources, checked 2026-10-28

10 sources
  1. With RLS enabled and no policy, Postgres applies a default-deny policy. Superusers and roles with BYPASSRLS skip row security, and table owners do too unless FORCE ROW LEVEL SECURITY is set.Source: postgresql.org, checked 2026-10-28.
  2. A table in an exposed schema without RLS is readable and writable by any role with a grant on it, and adding policies does not remove the grants. On existing projects a new public table starts with every privilege granted to anon, authenticated and service_role.Source: supabase.com, checked 2026-10-28.
  3. Supabase recommends one policy per operation. An update needs a select policy, with check limits the resulting row, policy columns need an index, views bypass RLS by default and security_invoker on Postgres 15 and above fixes it.Source: supabase.com, checked 2026-10-28.
  4. The secret key authorizes as the service_role Postgres role, which has bypassrls, and must never reach a browser. It skips RLS only when the request carries no user access token.Source: supabase.com, checked 2026-10-28.
  5. Default privileges on existing projects include truncate, references, trigger and maintain on Postgres 17, and new functions are callable by every role. The setting Automatically expose new tables controls the grants the Data API uses.Source: supabase.com, checked 2026-10-28.
  6. Supabase announced on 28 April 2026 that new tables in public stop being exposed to the Data API by default: 30 May 2026 for new projects and 30 October 2026 for existing ones. Existing tables, RLS and direct Postgres connections are unaffected.Source: github.com, checked 2026-10-28.
  7. Supabase advisor lints 0003, 0007, 0008, 0010, 0013, 0015, 0024 and 0028 cover the initplan wrapper, a policy on a table with RLS off, RLS with no policy, security definer views, RLS off in public, user_metadata in a policy, always-true policies and anon-executable security definer functions.Source: supabase.com, checked 2026-10-28.
  8. The publishable key is safe to expose and only reaches what RLS allows, while the secret key bypasses RLS. Supabase is deprecating the legacy anon and service_role keys by the end of 2026.Source: supabase.com, checked 2026-10-28.
  9. Storage allows no uploads to a bucket without RLS policies and is controlled by policies on the storage.objects table.Source: supabase.com, checked 2026-10-28.
  10. CVE-2025-48757 describes an insufficient database row-level security policy in Lovable through 2025-04-15 and notes that the supplier disputes it.Source: nvd.nist.gov, checked 2026-10-28.

Digiton Dynamics OÜ is registered in Tallinn, Estonia, and delivers from Lisbon.

Update log

  • First published. Every source was opened on 28 October 2026 and the three-state test was run the same day.

Each source above was opened on the date beside it. How facts get chosen and checked is written down in the editorial policy.

Talk to an engineer about your system

Book a free 25-minute scoping call. We tell you what we would build, what it takes to keep it running, and whether we are the right fit.