Building apps with AI
Supabase security checklist for apps built with Lovable, Bolt and v0
By Rocket Lab AustraliaPublished 11 min read

If your app was built with Lovable, Bolt, v0 or another AI tool on Supabase, its data is protected by Row Level Security (RLS), not by keeping the key in your app secret. The publishable (or legacy anon) key is designed to sit in the browser, so anyone who opens your site can use it. What that key can read and change is decided by the RLS policies on each table. Check six things before real users and real data arrive: RLS on every table, only the publishable key in the browser, storage bucket policies, auth settings, Edge Function secrets, and a test that proves one user cannot read another user's data.
This guide goes through each check with the setting or SQL to look at, linked to Supabase's own documentation. It ends with what to do if data has already been exposed.
Why this matters for AI-built apps
AI tools make it quick to get an app working. Whether it only works for the right person is a separate question, and a demo will not answer it. A table without RLS, or with a policy that lets everyone through, behaves perfectly in a demo: every screen loads and every form saves.
In 2025 a researcher scanned 1,645 apps from Lovable's public showcase and reported that 170 of them (about 10%) had database tables that could be read or changed through the public key. The finding was recorded as CVE-2025-48757; Lovable disputes it, on the grounds that each customer is responsible for protecting the data in their own app. Lovable's own documentation says the same: you are responsible for making sure your app meets the requirements of its use case. Whoever is to blame, the fix is the same, and it sits in your Supabase project.
The checklist
- RLS is enabled on every table the API can reach, with policies that limit each user to their own rows.
- Only the publishable (or legacy
anon) key is in browser or mobile code. The secret (or legacyservice_role) key is only on a server. - Every storage bucket has policies, and only files that are meant to be public sit in public buckets.
- Auth settings are set for production: email confirmations, exact redirect URLs, password rules and OTP expiry.
- API keys for other services (Stripe, OpenAI, email) live in Edge Function secrets, not in the front end.
- You have tested, as a signed-in user and as a visitor, that access to other people's data is blocked.
- The Security Advisor shows no errors.
1. Turn on RLS for every table
Supabase puts a REST API in front of your Postgres database. Any table in an exposed schema (usually public) can be reached with the publishable key, unless RLS says otherwise. Supabase's Row Level Security guide explains that once RLS is enabled, no data is accessible through the API with the publishable key until you create policies. So enabling it is the right first step, even before the policies are right.
alter table public.profiles enable row level security;
Then add a policy for each thing users should be able to do. A typical one lets signed-in users read only their own rows:
create policy "Users can view their own profile"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
Points to check in the policies your AI tool wrote:
- Policies that let everyone through. A policy with
using (true)for theanonorauthenticatedrole means any visitor, or any signed-in user, can read every row. Sometimes that is intended (a public list of products), often it is not. - Inserts and updates.
usingdecides which existing rows a user can see or change;with checkdecides which new or changed rows are allowed. Without awith check, a user may be able to write a row that claims to belong to someone else. - Roles kept in user metadata. A signed-in user can change their own
user_metadata, so Supabase warns it is not a good place for authorisation data. An "is admin" flag belongs inapp_metadataor in a table the user cannot write to. - Views. Views bypass RLS by default. On Postgres 15 and later, create them with
security_invoker = trueso they respect the policies on the tables underneath. - Tables that do not need to be in the API. Supabase's guide to securing the Data API recommends keeping internal tables in a schema that is not exposed, and lets you turn off automatically exposing new tables.
The quickest way to find gaps is the Security Advisor in the Supabase dashboard. Its errors include rls_disabled_in_public (a table anyone with your project URL can read, edit or delete), security_definer_view (a view that bypasses RLS) and sensitive_columns_exposed. It also warns about policies that exist on a table where RLS is still switched off: they do nothing until it is enabled.
2. Keep the secret key out of the browser
Supabase has two kinds of API key, as its API keys guide explains:
- The publishable key (starting
sb_publishable_), or the legacyanonkey. Low privilege and designed to be exposed in a browser or mobile app, because RLS limits what it can reach. - The secret key (starting
sb_secret_), or the legacyservice_rolekey. It bypasses RLS completely, so it must only ever be used on a server: an Edge Function, your own backend or a scheduled job.
When a query fails because of RLS, switching to the secret key makes the error go away, which is how it can end up where it does not belong. If that key ends up in front-end code, every visitor has full access to your database. Supabase rejects the new secret keys when they are sent from a browser, but a key copied out of your site still works from any other tool.
How to check:
- Environment variable names. Vite exposes variables that start with
VITE_to the browser (Vite docs), and Next.js does the same forNEXT_PUBLIC_(Next.js docs). A secret key must never have either prefix. - The deployed site. Open your live site, search its JavaScript files for
sb_secret_and for the value of yourservice_rolekey, and check your Git history for committed.envfiles. - One key per server component. Supabase recommends a separate secret key for each backend component, so a leak means rotating one key rather than all of them.
3. Lock down storage buckets
Files in Supabase Storage are protected by RLS policies on the storage.objects table. The storage access control guide says Storage allows no uploads without policies, so the risk is usually policies that are too broad, or private files in a public bucket.
- Public buckets are readable by anyone with the file's URL. Use them only for files that are meant to be public, like product images. Invoices, identity documents and anything users upload about themselves belong in a private bucket.
- Per-user folders. A common pattern keeps each user's files in a folder named after their user ID, with a policy that checks it:
create policy "Users upload to their own folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents' and
(storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
- Reads, updates and deletes need their own policies, scoped the same way. An upsert needs select and update policies as well as insert.
4. Check your auth settings
Supabase's production checklist lists the auth settings to review before launch. The ones to check first:
- Email confirmations turned on, so every account belongs to someone who controls its email address.
- Site URL and redirect URLs set to your production domain. The Site URL is the default destination of confirmation and password reset emails, and in a new project it points at a local development address. Supabase's redirect URL guide recommends exact URLs in production rather than wildcards.
- OTP expiry of one hour (3600 seconds) or less, and any test phone numbers removed.
- Password rules. A minimum length of at least 8 characters, and the check that rejects passwords already exposed in known breaches (Pro plan and above; see password security).
- Custom SMTP for auth emails, ideally on your own domain, so you control how reliably confirmation and reset emails are delivered.
- Multi-factor authentication for your own Supabase account, and for your users if the app holds sensitive data.
Also check that admin pages are protected on the server and in the database, not only by hiding a button in the interface.
5. Keep Edge Function secrets on the server
Keys for other services (Stripe, OpenAI, email providers) must never be in front-end code. Put them in Edge Function secrets and read them inside the function, as the Edge Function secrets guide shows:
supabase secrets set STRIPE_SECRET_KEY=sk_live_...
const stripeKey = Deno.env.get("STRIPE_SECRET_KEY");
- Secrets can also be set in the dashboard, on the Edge Function Secrets page.
- For local development, keep secrets in
supabase/functions/.envand add that file to.gitignore. - Edge Functions already receive your project's keys as built-in variables (such as
SUPABASE_URLandSUPABASE_SECRET_KEYS), so the secret key never needs to be copied anywhere else. - If you pasted a key into an AI tool's chat, treat it as exposed: move it into secrets and rotate it.
How to test that RLS actually blocks access
Policies that look right can still be wrong. Supabase's guide to testing Row Level Security describes two ways to prove they work.
From the app's side. Create two test users, A and B, each with some data. Sign in as A with the publishable key and try to read B's rows. Then do the same without signing in at all, as a visitor would. RLS filters rows silently rather than returning an error, so an empty result is a pass:
await supabase.auth.signInWithPassword({ email: userA.email, password: userA.password });
const { data } = await supabase.from("todos").select("*").eq("user_id", USER_B_ID);
expect(data).toHaveLength(0);
Updates and deletes also fail silently, so after trying to change B's rows as A, sign back in as B and check nothing changed.
In the database. With the Supabase CLI, write pgTAP tests that run as the authenticated role with a chosen user ID, and run them with supabase test db:
begin;
select plan(1);
insert into auth.users (id) values
('123e4567-e89b-12d3-a456-426614174000'),
('987fcdeb-51a2-43d7-9012-345678901234');
insert into public.todos (task, user_id) values
('User A task', '123e4567-e89b-12d3-a456-426614174000');
-- run as user B
set local role authenticated;
set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';
select is_empty(
$$ select 1 from todos where user_id = '123e4567-e89b-12d3-a456-426614174000' $$,
'User B cannot read User A todos'
);
select * from finish();
rollback;
Supabase also suggests proving that the tests can fail: add a temporary policy with using (true), check that a test goes red, then remove it. Run the tests whenever your AI tool changes the database, because a new table or a rewritten policy can quietly reopen access.
What to do if data was exposed
If you find a table, bucket or key that was open, work through these steps in order.
- Close the gap. Enable RLS, fix the policy or make the bucket private. This stops further access straight away.
- Rotate any leaked secret key. Follow Supabase's guide to rotating a leaked key: fix the cause of the leak, create a new secret key, move every component to it, then delete the old one. For a leaked legacy
service_rolekey, the guide also covers moving to the new keys, disabling the legacy keys and revoking the legacy JWT secret. The publishable key does not need rotating: it was always public, and RLS is what protects you. - Find out what was accessed. The Logs Explorer in the dashboard shows API gateway requests, which you can filter by path and time. How far back the logs go depends on your plan, so export what you need now.
- Work out whether you must notify anyone. In Australia, if your business is covered by the Privacy Act and personal information was exposed in a way likely to cause serious harm, the Notifiable Data Breaches scheme requires you to assess a suspected breach within 30 days and, if it is an eligible data breach, to notify the people affected and the Office of the Australian Information Commissioner as soon as practicable. Get legal advice on your situation.
- Fix the cause. Add the tests above, run the Security Advisor again, and review how the gap was introduced, so the next change from your AI tool does not reopen it.
How Rocket Lab can help
Our App Rescue Report is a fixed-price launch-readiness audit for apps built with Lovable, Bolt.new, Cursor, Replit, v0 and similar tools. Senior engineers check Supabase RLS, Firebase rules, exposed API keys and secrets in the front end, test sign-up, password reset and sessions, and check whether admin checks happen on the server. You get a prioritised list of fixes with hour estimates, and a clear view of what to fix before launch.
We have reviewed and fixed the Supabase security of apps built with AI tools, including their Row Level Security policies.Frequently asked questions
Is it safe to have the Supabase anon key in my app?
Yes. The publishable key, and the legacy anon key it replaces, are designed to be public. They are only as strong as your RLS: with it enabled and the right policies, the key can reach only what each user is allowed to see. The secret key, or legacy service_role key, is different and must never be in the app.
Is RLS turned on by default?
Do not rely on it. Depending on how a table was created, and by which tool, RLS may be off. Check every table in the Security Advisor, which flags tables in the public schema without it.
Does Lovable's built-in scan replace a review?
It is a useful first check: Lovable's scan flags tables without per-record access control and policies that let everyone through. Lovable itself says its tools cannot guarantee an app is fully protected, and recommends a professional review for apps that handle sensitive data. A scan also cannot tell whether a policy matches what your business intends, such as which staff should see which customers.
Should I rotate the anon key if it was exposed?
No. It is meant to be public, so a new one would be just as public. Fix the RLS that let it reach too much. Rotate the secret or service_role key if that was exposed.
Do I need to rebuild my app to fix this?
Usually not. RLS, storage policies, auth settings and secrets can all be fixed in the existing Supabase project. A rebuild is worth considering only when the data model itself is wrong, and an audit tells you which case you are in.
Talk to us about your app
- Get it checked. Book an App Rescue Report and we will review your app's data access, sign-in, architecture and deployment before more users and data go through it.
- Talk to the team. Book a free 30-minute consultation to talk through your app and what it needs before launch.
Ava, our AI assistant, can help you put a project brief together.
Scope your project


