FixLoop

Is my Supabase database public?

Guide · October 14, 2026

You shipped a Lovable app. The anon key is in the browser (it is supposed to be). The scary question is not "is the key exposed." It is: can a stranger read a table with that key and no login?

If Row Level Security is off on a table that still has grants to anon, the answer is yes. The Data API is a public database. This is a 5-minute check you can run on your own project.

Same model in the Row Level Security and Securing your API docs.

What "RLS off + anon key in the client" means

The anon key is a public JWT. Lovable (and Vite, Next, the rest) put it in env vars like VITE_SUPABASE_ANON_KEY or NEXT_PUBLIC_SUPABASE_ANON_KEY so the browser can talk to Supabase. Anyone who loads your app can copy it. That part is normal.

Postgres then decides what that key is allowed to do:

  1. Grants decide whether anon can SELECT / INSERT / UPDATE / DELETE the table at all.
  2. RLS decides which rows survive that grant.

RLS off means step 2 never runs. If anon also has a grant (the old default on public tables), every row is reachable from the REST endpoint. Emails, orders, chat messages, whatever you stored.

A policy of using (true) is the same shape of hole with RLS "on." The dashboard badge looks fine. The table is still public.

The 5-minute check

Do this on your project, from a private window so you are not accidentally sending a user session.

1. Copy the project URL and anon key

Easiest path: open the live app, DevTools → Network, filter rest/v1 or supabase.co. Click a request.

No traffic yet? Check Lovable env / a local .env for SUPABASE_URL and the anon key. Do not use the service_role key. That one bypasses RLS and must never ship to the client.

2. Read a table with no login

Pick a table the app actually uses. Names from the network tab beat guessing. Common Lovable ones: profiles, users, messages, todos, orders.

curl "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=5" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

Same thing from the browser console on any blank tab:

const res = await fetch(
  "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=5",
  {
    headers: {
      apikey: "YOUR_ANON_KEY",
      Authorization: "Bearer YOUR_ANON_KEY",
    },
  }
);
console.log(res.status, await res.json());

No cookies. No supabase.auth.getSession(). If this returns user data, a stranger can do it too.

3. Read the result

Stop after a handful of rows. If you see other people's data, turn RLS on before you paste anything into a chat window.

The safe fix

Turn RLS on, then add policies that filter by auth.uid(). Grants stay least-privilege. Do not "fix" an open table by granting more.

alter table public.your_table enable row level security;

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

create policy "insert own rows"
on public.your_table
for insert
to authenticated
with check (auth.uid() = user_id);

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

grant select, insert, update, delete on table public.your_table to authenticated;
grant select, insert, update, delete on table public.your_table to service_role;

Swap user_id for whatever column stores the owner. After RLS is on with no matching policy, even you get [] until the policy is right. That is safer than open.

Do not do this:

Leave anon with nothing unless the table is meant to be public (a published posts list, for example). Then GRANT SELECT only, RLS on, and a policy that matches that intent.

In Lovable, put the same rules in the project instructions so the next migration does not recreate the hole: enable RLS, policies using auth.uid(), no GRANT ALL to anon.

Quick checklist

Questions

Is the Supabase anon key supposed to be in the frontend?

Yes. The anon key is designed to ship in the browser. Finding it in env or the Network tab is not the leak. The leak is RLS disabled (or a wide-open policy) while that key can call the Data API.

What does it mean if the REST call returns rows with no login?

The anon role can SELECT that table. Either RLS is off, or a policy allows anonymous reads. If those rows look like user data, the table is public.

I got an empty array. Am I safe?

Not by itself. [] means there is a SELECT grant and RLS returned no rows, or the table is empty. Retry a table you know has data. If the logged-in app shows rows and anon still gets [], signed-out visitors are probably blocked.

What do 401 and 42501 mean on this check?

401: the key is missing, truncated, or not a valid JWT. Retry with a clean anon key. 42501: anon has no grant, so the Data API cannot touch the table. That is the grant layer, not RLS. Details in the Oct 30 grants guide.

Lovable says RLS is on. Can the table still be public?

Yes. A policy with using (true) (or to anon plus a condition that always matches) still returns rows. Run the REST check. The response is the source of truth, not the dashboard badge.

Should I rotate the anon key if I found it in the client?

Not for that reason. It is meant to be there. Rotate if you also shipped the service_role key, or if you need to revoke a leaked service role. Fixing RLS does more than rotating anon.

Want a second pair of eyes? Paste the public URL into the free scan for leaked keys and missing headers. The REST check above is still how you prove a table is open.

Run a free security scan