Fix · My app's database is readable by anyone: how to find and fix it

Supabase anonymous users can read and write my tables

A request from a browser console, or from a visitor who is not signed in, reads or changes rows in your Supabase tables. Or a test user edits rows that belong to someone else. The usual cause is a table in an exposed schema with row-level security off, or a policy, function, column privilege or anonymous sign-in that lets the request through.

Likely causes, most common first

Cause 1 · in 1 of 2 matching cases

RLS is off on a table in an exposed schema

A table in an exposed schema without RLS is readable and writable by any role with a grant on it. The securing-your-API guide adds that a table is reachable through the Data API once a role has been granted privileges on it.

How to tell: The linter reports 0013_rls_disabled_in_public for the table, or a request without a user session returns its rows.

Cause 2 · in 1 of 2 matching cases

Functions, views or columns sit outside the table policies

By default, the anon role can run a function, and a view created by the postgres user bypasses RLS. RLS also does not control which columns a row owner can update. So a function that sets a flag, or a plan column on a subscription row, can be changed even when the table policies look right.

How to tell: On a test project, call the function with rpc and no session, or update the plan column on your own row as a test user. If either succeeds, this is the cause.

Cause 3

A policy is open to anon

A policy that reads to anon using ( true ) gives every unauthenticated visitor read access to every row the role can already reach through grants. Grants control whether a role can access an object, and RLS controls which rows the role can access.

How to tell: A policy's condition reads true, or the policy names the anon role, and a request with no session returns rows.

Cause 4

Anonymous sign-ins pass policies written for signed-in users

A visitor who signs in anonymously assumes the authenticated role, the same as a permanent user. A policy written for the authenticated role therefore lets them in. It is easy to miss when you tested the rules with a normal sign-in.

How to tell: Sign in anonymously in a test client and repeat the failing read or write. If it succeeds, and the same request with no session fails, this is the cause.

Check and fix it, step by step

  1. Run the database linter and read the result for lint 0013

    Run the advisors for your project and look for 0013_rls_disabled_in_public, which the linter describes as a publicly accessible table. The flagged tables give you a worklist before you change anything.

    Docs: supabase.com →

  2. Repeat the failing request as anon and as an anonymous user

    A request with no signed-in user runs as anon. An anonymous sign-in runs as authenticated and carries an is_anonymous claim. Note which role can read and which can write each table, so you know which policy to fix.

    Docs: supabase.com →

  3. Enable RLS on the flagged tables

    Run the statement for each flagged table in an exposed schema, replacing the table name with yours. Until you add policies, no data is accessible through the API with a publishable key, so expect empty results. The next step adds policies.

    alter table public.reports enable row level security;

    Docs: supabase.com →

  4. Replace open policies with row conditions

    Replace any policy whose condition is true for every row. The new policy should name a role and tie each row to the requesting user. Grants control whether a role can access a table. The policy controls which rows.

    create policy "Users read own rows"
    on public.reports
    for select
    to authenticated
    using ( (select auth.uid()) = user_id );

    Docs: supabase.com →

  5. Keep anonymous sessions out of write paths

    If anonymous sign-ins are on, add a restrictive policy so inserts need a permanent user, and review existing policies before you rely on them. Enable CAPTCHA or Cloudflare Turnstile on the sign-in endpoint, because it can be abused to grow your database.

    create policy "Permanent users can insert"
    on public.reports
    as restrictive
    for insert
    to authenticated
    with check ( (select (auth.jwt()->>'is_anonymous')::boolean) is false );

    Docs: supabase.com →

  6. Revoke execute on functions that write

    Functions in an exposed schema can be called with rpc, and by default the anon role can run them. Revoke execute from public and anon. Grant it back to authenticated where the app needs it, and check ownership inside security definer functions.

    revoke execute on function public.hello_world from public;
    revoke execute on function public.hello_world from anon;

    Docs: supabase.com →

  7. Restrict which columns users can update

    RLS decides which rows a user can update, not which columns. A row-ownership update policy gives the owner access to every column. Revoke table-level update, then grant it on the columns users may change.

    revoke update on table public.subscriptions from authenticated;
    grant update (display_name) on table public.subscriptions to authenticated;

    Docs: supabase.com →

Quick check: Run the Supabase database linter and look for 0013_rls_disabled_in_public on your public tables.

How often this shows up in our data

2 of the 937 verified cases from the last 12 months in our data match this symptom (0.2%). The most common cause was “RLS is off on a table in an exposed schema” (1 of 2). How we collect and verify cases.

With Gemmein

On Gemmein, each collection is governed by one plain-English rule for who may read and write it, and security (tenant isolation, rate limiting, audit logs, domain-locked keys) is on by default with nothing to configure. A relay's run_function action runs a pushed Edge function with the caller's own rights when a signed-in person triggered the relay, or only the reach you approved when nothing did, and the Errors page lists every write Gemmein refused.

Questions

Why can an anonymous user write when I thought anon was blocked?

An anonymous sign-in creates a user who assumes the authenticated role, which is not the anon API key. A policy written for authenticated applies to that user, so check the is_anonymous claim where writes matter.


I enabled RLS and now my app returns no rows. Is that expected?

Yes. Once RLS is on, no data is accessible through the API with a publishable key until you create policies. Add a policy per table and operation.


Can I turn off the Data API instead of writing policies?

If the app does not use Supabase client libraries or the REST or GraphQL data endpoints, you can turn the Data API off. Its auto-generated REST endpoints then stop responding, regardless of grants or RLS.


Sources

Every cause and step above was checked against these pages on 10 Nov 2026.

The broader pattern

This is one symptom of a wider failure pattern: My app's database is readable by anyone: how to find and fix it. The guide covers every cause we see for it, on any stack.

Get a heads-up when Supabase breaks something

Supabase

Leave your email and we'll let you know when something big changes for Supabase. Unsubscribe any time by replying. Gemmein Limited. Research terms · Privacy

← All fixes