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:
- Grants decide whether
anoncanSELECT/INSERT/UPDATE/DELETEthe table at all. - 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.
- Host looks like
abcdefghij.supabase.co. - Path looks like
/rest/v1/profiles(that last segment is the table name). - Request headers:
apikeyandAuthorization: Bearer …. For signed-out traffic those two values are the anon key.
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
- 200 and an array of rows.
anoncan read the table. If you see emails, names, contents, payment fields, treat it as public. RLS is off, or a policy allows anonymousSELECT. - 200 and
[]. The Data API can see the table (there is aSELECTgrant) but no rows passed RLS, or the table is empty. Retry a table you know has data. If the signed-in app shows rows and anon still gets[], signed-out visitors are probably blocked. That is the usual healthy case. - 401. Missing, truncated, or invalid key. Fix the header and retry. A 401 here does not mean the table is private.
- 403 /
42501/permission denied for table.anonhas no grant, so the Data API never reaches RLS. The table is not exposed to that role. After October 30, 2026 this shows up a lot on new tables. Different problem: the grants change guide. - 404 /
PGRST205. Wrong table name, or it is not in the API schema cache. Use the name from the network tab.
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:
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon;- Any grant to
anonwhile RLS is still off - A policy of
using (true)on a table with private rows - Putting the
service_rolekey in Lovable frontend env
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
- Anon key in the client is expected. RLS off (or
using (true)) is the actual problem. - Copy the key from a signed-out Network request, not from a logged-in session.
- Hit
/rest/v1/<table>?select=*&limit=5withapikey+Authorization: Bearer. - Rows with private fields: enable RLS and lock policies to
auth.uid()before you do anything else. []on a table that has data: usually RLS is on for anon. Confirm on more than one table. Logged-in writes that "save" and revert are the silent update guide.- 401: bad key. 42501: no grant (see the Oct 30 grants guide).
- Never
GRANT ALLtoanon, and never grant toanonwith RLS off.
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