Guides / Is my Lovable app secure? (Supabase RLS checklist)
Is my Lovable app secure? (Supabase RLS checklist)
Lovable ships real security tooling now. It still cannot know which rows your users should see. Here is the 20-minute check we run before a Lovable app goes public.
1. Run both built-in scans, not just the automatic one
Lovable has two scans. The Quick scan runs automatically when you open the publish dialog and checks database access rules, npm dependencies and MCP server exposure. The Deep scan reviews your actual application code (access control, unauthenticated endpoints, injection, leaked secrets, payments, auth) and does not run on its own.
- Open your project's Security view and click Run deep scan.
- Findings are labeled Critical, Warning or Info. Fix every Critical before publishing.
- Running scans is free. Fixing with Try to fix all uses free fixes first, then credits.
2. Read your RLS policies yourself
If you use Lovable's built-in backend, every policy is listed under More → Cloud → Database → RLS policies. If your app is connected to your own Supabase project, open the SQL Editor in the Supabase dashboard and run:
-- 1. Which public tables have RLS off? select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename; -- 2. What do the policies actually allow? select tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public' order by tablename, cmd;
What you are looking for:
- Any row with
rowsecurity = false. Supabase's docs are blunt: a table in an exposed schema without RLS is readable and writable by any role with a grant on it. - Policies where
qualorwith_checkis justtrueandrolesincludesanon. That means every signed-out visitor gets that operation on every row. Fine for a public blog table, a breach forordersorprofiles. - Policies with no role (
rolesshows{public}). Supabase recommends always naming the role withto authenticatedorto anon. - Update policies whose
with_checkis weaker thanqual, such aswith check (true). That can let a user move a row they own to another user. A blankwith_checkon an update policy is not by itself a hole: Postgres reuses theusingexpression as the check (see section 3).
3. The owner-only pattern, written correctly
For a table where each user only touches their own rows, Supabase's documented pattern is one policy per operation:
alter table public.profiles enable row level security; create policy "Users can view their own profile." on profiles for select to authenticated using ( (select auth.uid()) = user_id ); create policy "Users can create their own profile." on profiles for insert to authenticated with check ( (select auth.uid()) = user_id ); create policy "Users can update their own profile." on profiles for update to authenticated using ( (select auth.uid()) = user_id ) with check ( (select auth.uid()) = user_id ); create policy "Users can delete their own profile." on profiles for delete to authenticated using ( (select auth.uid()) = user_id );
Wrapping auth.uid() in (select ...) is Supabase's recommended form: it lets Postgres cache the value per statement instead of calling it per row. More detail in our Supabase RLS guide.
How using and with check actually work
usingdecides which existing rows a command can see or touch (select, update, delete).with checkdecides whether the new version of a row is allowed (insert, update).- If an update policy has no
with check, Postgres uses itsusingexpression for both. Sousing (auth.uid() = user_id)alone still blocks a user from changinguser_idto someone else. Writing both out, as above, is clearer, not mandatory. - Permissive policies (the default) are OR-ed together. One extra
using (true)select policy opens every row, no matter how strict the others are. Restrictive policies (as restrictive) are AND-ed with the permissive ones and can never grant access alone.
Where the real update risks are:
- An explicit
with checkweaker thanusing, e.g.with check (true). In our test this let a user hand their row to another user once a broadusing (true)select policy also existed (Postgres also checks the new row against select policies when the update reads columns, which blocked it while select was owner-only). - RLS checks rows, not columns. An owner-only update policy still lets the owner set
is_admin = trueorplan = 'pro'on their own row. Fix it with column privileges:revoke update on profiles from authenticated; grant update (display_name) on profiles to authenticated;, or move privileged fields to a table users cannot write. using (true)or notorole on private tables.
Test, don't trust. In the SQL editor, impersonate a user inside a transaction and try the attack:
-- Attempt 1: can user A see or steal other rows?
begin;
set local role authenticated;
select set_config('request.jwt.claims',
'{"sub":"<user-a-uuid>","role":"authenticated"}', true);
select * from profiles; -- should show only user A's rows
update profiles set user_id = '<user-b-uuid>'
where user_id = '<user-a-uuid>'; -- should fail (RLS error, or permission denied if column grants exclude user_id)
rollback;
-- Attempt 2 (separate transaction): can user A make themselves admin?
begin;
set local role authenticated;
select set_config('request.jwt.claims',
'{"sub":"<user-a-uuid>","role":"authenticated"}', true);
update profiles set is_admin = true
where user_id = '<user-a-uuid>'; -- should fail once column privileges are set
rollback;
We ran every SQL example in this guide against PostgreSQL 18.4 with a stub auth.uid() reading request.jwt.claims, the way Supabase does. Results matched the behavior described above.
4. Never authorize on user_metadata
Supabase warns that raw_user_meta_data can be updated by the signed-in user. A policy like "admins can read everything" that checks a role stored there lets any user promote themselves. Store roles in app_metadata or in a table the user cannot write.
5. Keep secret keys out of the frontend
Lovable builds browser code, and anything in the browser is public. Lovable detects API keys pasted into chat and steers you to store them in Secrets and call the service from server-side code. Check that nothing slipped past:
- The Supabase publishable key (
sb_publishable_...) or legacy anon key in the browser is expected. RLS is what protects you. - A secret key (
sb_secret_...) or legacy service_role key in the browser is critical. It bypasses RLS completely. Rotate it (see our rotation guide). - Open your live site, view the JS bundle in DevTools, and search for
sb_secret_,service_role,sk_live_andsk-.
6. Turn on leaked password protection
If users sign in with email and password: on Lovable Cloud, enable Password HIBP check under More → Cloud → Users → Auth settings → Email. On your own Supabase project, use Supabase's leaked password protection setting.
Pre-launch checklist
- Deep scan run, all Critical findings fixed
- Every public table:
rowsecurity = true - No
anonpolicy withtrueon private data - No update policy with a
with checkweaker than itsusing - Privileged columns (roles, plan, balance) not user-updatable
- No role checks on
user_metadata - No secret or service_role key in the JS bundle
- Leaked password protection on
next step
Want a second pair of eyes?
Grab the free Lockdown Pack (security rules for Cursor and Claude Code plus a pre-launch checklist), or have us check the whole app for a fixed $99.
Sources
Every product step above was checked against these official pages. Last checked Oct 2026. Products change; if a menu moved, the docs below are the source of truth.