Supabase database

How to secure a Lovable app’s Supabase database

To secure a Lovable app’s Supabase database, make sure every table has row level security on, replace any policy that lets everyone read (using (true)) with one that checks the row’s owner, then test each table as a logged-out visitor and as a second signed-up user. This guide walks through each step with SQL you can run, plus the keys, storage and functions that sit around the database.

TL;DR

  • The Supabase key in your Lovable app’s frontend is public. Policies decide what it can read, so the policies are your security.
  • Run two queries: tables with RLS off, and read policies that never mention auth.uid(). Those are your leak candidates.
  • Fix with owner-only policies in a new migration, then test as anon, a stranger and the owner.
  • Then check the rest: no service role key in the code, private files in private buckets, Edge Functions that check who is calling.

How a Lovable app talks to Supabase

An app built with Lovable is usually a React and Vite frontend with Supabase as the backend. The frontend creates a Supabase client with your project URL and the public anon (or publishable) key, typically in a generated file such as src/integrations/supabase/client.ts, and queries tables straight from the browser.

That key ships to every visitor, and it is meant to: Supabase is designed for it. What keeps data private is row level security, the per-table rules Postgres applies to every query. If a table holding emails or phone numbers has no rule, or a rule that says yes to everyone, anyone can copy the key from your site and read the whole table. Nobody has to break in.

If your Lovable project is connected to GitHub, the database changes it made are usually saved as SQL files in supabase/migrations. That folder is the record of your rules, and it is what you’ll add the fix to.

Step 1: find tables with RLS switched off

Open your project’s SQL editor in the Supabase dashboard and run:

find-rls-off.sql
-- Tables in public with row level security switched off
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;

Every table listed here has no row rules at all. If the API roles have been granted access (older projects grant it by default), anyone with your key can read and change every row. Switch RLS on for each one; Step 3 adds back what the app needs.

Step 2: find policies that let everyone read

find-open-policies.sql
-- Read policies that don't check who is asking
select tablename, policyname, roles, qual
from pg_policies
where schemaname = 'public'
  and cmd in ('SELECT', 'ALL')
  and coalesce(qual, '') not ilike '%auth.uid()%';

Look at the roles column. {public} or {anon} means logged-out visitors are included. {authenticated} with qual = true means anyone who signs up, which for an app with open sign-up is the same thing. Some of these will be fine: a product list or blog posts are meant to be public. The ones that matter are tables with personal data: profiles, users, customers, orders, bookings, messages, leads.

Step 3: replace them with owner-only policies

For a table where each row belongs to a user (a user_id or id column that matches the user’s id), the fix is a policy that compares that column with the signed-in user. Add it as a new migration file so it is versioned with the rest of your schema, for example supabase/migrations/20261110120000_lock_profiles.sql:

supabase/migrations/…_lock_profiles.sql
alter table public.profiles enable row level security;

drop policy if exists "Profiles are viewable by everyone" on public.profiles;
create policy "Users read own profile" on public.profiles
  for select to authenticated
  using ((select auth.uid()) = id);
create policy "Users update own profile" on public.profiles
  for update to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);

Use the real policy name from Step 2 in the drop policy line. If part of the table genuinely needs to be public (a seller’s display name on a listing), keep rows readable and withhold the private columns instead; the RLS guide shows how.

Asking the AI to fix it? Be specific: “Add a migration so users can only read and update their own row in profiles, using auth.uid() = id. Do not use USING (true) and do not use the service role key in the frontend.” Then test the result yourself (Step 4). A vague “fix the security” can produce a policy that hides the error without closing the leak.

Step 4: test as a visitor, a stranger and the owner

Don’t trust the policy text: run the queries as each kind of user. The three-identity script in the RLS guide does this for any table, inside transactions that change nothing. For each table with personal data you want:

  • logged-out visitor: 0 rows
  • a signed-in stranger who owns nothing: 0 rows
  • the owner: their own rows, and nothing else

Then click through the app as a normal user. If a page breaks, the frontend was relying on the open access, which usually means it was showing users data that wasn’t theirs. Fix the query rather than reopening the table.

Step 5: check the keys in your code

A Supabase project has a second key, the service role key, which ignores every policy. It belongs only on servers. Search the repo for it:

check-keys.sh
grep -rnE "service_role|SERVICE_ROLE|sb_secret_" --exclude-dir=node_modules .
# Any hit in src/ or in a VITE_ variable means it ships to the browser

If you find it, rotate it in the Supabase dashboard first, then move whatever needed it into an Edge Function. The same goes for AI and payment keys pasted into frontend code. How to rotate a leaked key and clean git history.

Step 6: storage, functions and sign-up

  • Storage. Avatars can live in a public bucket. ID documents, invoices and anything personal belong in a private bucket with per-user folder policies. Storage bucket policies.
  • Edge Functions. Any function using the service role key must check who is calling, and take the user’s id from their token, not the request body. Securing Edge Functions.
  • Sign-up. Keep “Confirm email” on in your project’s authentication settings, and never decide access from an email address or from user_metadata. Where to store roles.
  • Views. A view over a locked table can skip its policies. Views and security_invoker.

Step 7: keep it fixed

Every time the AI changes the database, it can change a policy. Two habits keep you safe:

  1. Run Supabase’s Security Advisor in the dashboard after each round of changes. It catches RLS switched off, functions without a fixed search path, and more. It deliberately doesn’t judge public reads, because only you know whether a table is meant to be public.
  2. Re-test the tables with personal data as a stranger after each change, or have it done automatically on a schedule.

Before launch, work through the pre-launch checklist.

Sources

PatchLabs is independent and not affiliated with Lovable or Supabase. This guide describes apps built on them.

The checks behind this guide, each with example code, a self-check and the fix.

Free check

Check your app for this, free.

Paste your public repo. PatchLabs rebuilds your database from its migrations, tests it as a visitor, a stranger and the owner, reads the code for keys and risky patterns, and hands you the fix.

Free for your first appNever touches your database

Written by Manraj Singh Dhillon. Last checked 10 Nov 2026. Code samples are synthetic and run against your own project; nothing here names a real app. Spotted a mistake? Tell us.