The short version
- Tables: row-level security on, with policies, tested using the public key.
- Keys: only the anon or publishable key in the front end. Secrets live in Edge Function secrets.
- Edge Functions: check who's calling before doing anything paid or private.
- Storage and auth: private buckets by default, and redirect URLs limited to your domains.
1. How these apps are put together
Your front end is a React app that runs entirely in the browser. It talks to Supabase directly using the project URL and a public key, both of which are visible to every visitor. That's normal and by design.
So the question isn't whether someone can find your Supabase URL and key. They can. The question is what that key lets them do, and the answer is decided by your database policies, not your UI.
2. Check every table has row-level security
In the Supabase dashboard, open the Policies page. Every table in the public schema should show RLS enabled with at least one policy. Supabase's Security Advisor also flags tables without RLS.
Then test it the way an outsider would, using only your public key:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=5" \
-H "apikey: YOUR_ANON_OR_PUBLISHABLE_KEY"If that returns other users' rows, anyone can do the same. Turn on RLS and add policies with the RLS guide. Repeat for each table that holds anything private.
3. Make sure no secret keys reached the front end
Search your project for service_role, sb_secret_, sk- and sk_live_. None of them belong in front-end code or in a VITE_ variable. See which keys are safe in the browser.
Calls to OpenAI, Stripe or email providers belong in a Supabase Edge Function, with the key stored as a Supabase secret. If a secret was ever in the front end, rotate it.
4. Check your Edge Functions
An Edge Function's URL is public. Before it does anything private or paid, it should confirm who's calling:
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
// ...only now call OpenAI, Stripe, etc. with keys from Deno.env
return new Response("ok");
});For anything that calls an AI model, add a per-user limit too. See rate limiting AI routes.
5. Check storage buckets
In Storage, look at each bucket. A public bucket serves every file to anyone with the URL, which is right for logos and public images and wrong for invoices, uploads or profile documents.
Make private buckets the default, and add policies on storage.objects so users can only read and write their own folder.
6. Check auth settings and admin pages
- Redirect URLs: in Authentication settings, the Site URL and allowed redirect URLs should list only your real domains, not wildcards you added while testing.
- Email confirmation: decide on purpose whether new accounts must confirm their email.
- Admin pages: hiding an admin button in the UI doesn't protect anything. Admin-only data needs RLS policies that check the user's role on the server side.
Before you ship
- RLS on for every public table, with policies, tested with the public key.
- No service role, secret or AI keys in front-end code or VITE_ variables.
- Edge Functions check the user before doing anything private or paid.
- Private buckets by default, with per-user storage policies.
- Auth redirect URLs limited to your real domains.
- Admin access enforced by policies, not by hiding UI.
Check your live site in 60 seconds
ShipReady scans your deployed URL and the JavaScript visitors actually download: leaked keys, Supabase exposure, unprotected API routes, titles and share images. Secret findings unlock only after you verify you own the site.
Free scan, no signup, no code upload.FAQ
Lovable has its own security scan. Do I still need this?
Use it, it catches a lot. Then verify the deployed app: test a table with the public key and check what your live site actually serves. Scans of the project and checks of the live site catch different things.
Does this apply to Bolt, v0 or Replit apps?
Yes, for any app that uses Supabase from the browser. The table, key, function and storage checks are the same. Apps with their own server still need the key and rate-limit checks.
I found an open table. Is my data already gone?
Not necessarily, but assume it could have been read. Turn on RLS now, then check Supabase's API logs for unusual requests to that table.
Keep reading
Sources: Supabase: Row Level Security · Supabase: Securing Edge Functions · Supabase: Storage access control