Lovable
Lovable apps and Supabase row level security
The anon key in a Lovable app is public. Row level security, not a hidden key, decides who can read your Supabase tables.
Updated 2026-10-27
A Lovable app that talks to Supabase is safe only when row level security is enabled and each policy matches what that visitor is allowed to see. The anon key in the generated client is not a secret.
Lovable's Supabase connector writes the project URL and the anon key into the frontend so the browser can call PostgREST. Anyone who can open the app can read that key out of the JavaScript bundle. That is how the hosted client is designed to work. Hiding the key in a VITE_ variable does not change it: Vite inlines those variables into the build.
The key that must never appear in that bundle is the service role key. A service role JWT carries the role service_role. Supabase treats that role as a bypass of row level security. If Lovable, a chat prompt, or a copied snippet puts that key in client code, rotate it in the Supabase dashboard and remove it from the repo before you do anything else. Vibe Scan flags it by decoding the JWT payload and reading the role claim. It does not need a live database call to see the problem.
What to turn on
In the Supabase SQL editor, enable row level security on every table the API exposes:
alter table public.profiles enable row level security;
Then add policies per action. A policy with using (true) for select means every anon request can read every row. That is public data, even though RLS is "on". A useful starter for a profile row the signed-in user owns is auth.uid() = user_id on select, insert, update, and delete. Writes need a with check expression too, or Postgres will refuse the insert.
Tables in the public schema are candidates for the Data API. Grants matter as well as policies. If RLS is off and anon or authenticated still has select, PostgREST will return rows to anyone holding the anon key. New Supabase projects have sometimes shipped with broad grants. Check both.
Views and preview mode
A view that reads a protected table can still leak rows if the view runs as the owner. On current Postgres, prefer with (security_invoker = true) so the view obeys the caller's RLS. A Lovable preview that "could not read profiles" is often fixed by someone pasting disable row level security into a migration. That makes the preview work and makes the production API world-readable. Fix the policy instead.
Server-only work, such as a webhook from Stripe or an admin task, belongs in a Lovable server function or an Edge Function that reads the service role from the host's secret store. The browser bundle should create the Supabase client with the anon key only.
FAQ
Is the Supabase anon key supposed to be in the Lovable app?
Yes. It identifies the project to PostgREST. Protection is row level security and grants, not secrecy of the anon key.
What if the service role key is already in the bundle?
Treat it as public. Revoke it in the Supabase dashboard, issue a new one, and keep the new one in server-side secrets only. Assume rows were readable while it was exposed.
Does a policy that checks auth.uid() protect logged-out visitors?
It protects rows from the anon role only when no policy grants that role access. If you also add a policy for anon with using (true), logged-out visitors can read those rows.