Supabase

Supabase row level security for client-built apps

The anon key is public. RLS off, a policy of true, or a view that runs as the owner will expose tables through the Data API.

Updated 2026-10-27

Supabase exposes tables in the public schema through PostgREST at /rest/v1. The anon key is the credential a browser is expected to hold. If row level security is off, that API returns rows to anyone who has the key, which means anyone who loaded your app.

Row level security is a Postgres feature. When it is enabled and no policy matches, Postgres returns zero rows. When a policy's using expression is true, Postgres returns every row to that role. "RLS enabled" is not the same sentence as "data is private".

Grants and policies

PostgREST uses the database role inside the JWT: anon for a logged-out client, authenticated for a signed-in user, service_role for your server. The service role bypasses RLS. That is why it stays in a secret store and never in a Vite or Next client bundle.

On the roles that do go to the browser, check two layers:

  1. Table grants. anon should not have privileges you did not mean to give. A grant of select without RLS is a public table.
  2. Policies, one set per command you want to allow. select uses using. insert and update also need with check. delete uses using.

A typical owner policy compares user_id to auth.uid(). Policies for public content, such as a blog post with published = true, should say that explicitly. Avoid copying a dashboard template that uses using (true) on a profiles or payments table.

Storage buckets have their own policies. An open bucket is a file leak even when every table is locked.

Views, functions, and the schema cache

A view owned by a privileged role can leak the underlying table unless it is created with security_invoker = true. A security definer function that returns rows is a bypass if the API can call it. Do not grant execute on those functions to anon unless the function enforces its own checks.

After you change policies, the Data API follows the new rules on the next request. You do not need to hide the project URL. URLs like https://your-project.supabase.co are public configuration.

Vibe Scan's deep check, which runs only after you prove you control the domain, sends HEAD with Prefer: count=exact and the anon key. A success means the anon role was allowed to count that table. The scanner does not request a row body and does not store one. A failure, including 401 or 403, is treated as closed.

FAQ

Does enabling RLS stop the service role from reading the table?

No. The service role bypasses RLS. Keep that key on the server. Rotating it is the response if it ever reaches a client bundle.

Our policy is using (auth.uid() = user_id) but the table still looks open. Why?

Another policy for anon or authenticated may also match, and policies are combined with or. Drop the broad policy. Also check views and grants.

Is the anon key in the JavaScript a vulnerability by itself?

No. It is a finding only as information, unless the JWT role is service_role or a later check shows the anon key can count a sensitive table.