Guide
Supabase security checklist before you launch
If you built your app with Lovable, Bolt, Cursor or v0 and it talks to Supabase, your browser code talks straight to your database. That’s fine and it’s how Supabase is designed to work, but it means two things decide whether strangers can read your data: which key you ship, and your Row Level Security (RLS) policies.
This is the checklist we’d run on our own app before launch. Every step is something you can check yourself in a few minutes. Only run the tests below against an app you own.
1. Make sure the right key is in your frontend
Supabase has two kinds of keys, and only one of them can be public.
| Key | Looks like | Okay in browser code? |
|---|---|---|
| Publishable key | sb_publishable_... | Yes, as long as RLS is set up |
Legacy anon key | Long JWT starting with eyJ | Yes, as long as RLS is set up |
| Secret key | sb_secret_... | Never |
Legacy service_role key | Long JWT starting with eyJ | Never |
Secret and service_role keys skip RLS completely. If one of them is in your site’s JavaScript, anyone can read, change or delete every row, no matter how good your policies are. Supabase’s own docs put it simply: the publishable key only opens the lobby, the secret key is the master key.
The tricky part is that the legacy anon and service_role keys look the same at a glance. Both start with eyJ. To tell them apart, paste the key into your browser console like this and look at role:
const k = "eyJ...your key...";
JSON.parse(atob(k.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))).role
// "anon" is fine. "service_role" means stop and fix it now.
To find which key your live app ships, open it, open DevTools, go to the Network tab, and look at any request to your-project.supabase.co. The apikey header is the key your frontend uses. If you have the code locally, you can also search your build output:
npm run build
grep -rlE "sb_secret_|service_role" dist/
A legacy service_role key won’t contain the words “service_role” in plain text, since they’re base64-encoded inside the JWT. Decoding the key, as shown above, is the reliable check.
Supabase says the legacy anon and service_role keys are being deprecated by the end of 2026, so if you’re touching this code anyway, it’s a good time to move to publishable and secret keys.
2. Turn on RLS for every table
Every table in the public schema can be reached through your project’s API. If RLS is off on one of them, anyone with your project URL and public key can read and write it. Run this in the Supabase SQL editor:
select tablename
from pg_tables
where schemaname = 'public'
and not rowsecurity;
Any table it returns has RLS off. Supabase’s Security Advisor (in the dashboard under Advisors) flags the same thing as an error. Fix it with:
alter table public.your_table enable row level security;
Once RLS is on, the public key can’t see anything in that table until you add a policy that allows it. That’s the safe default. Add policies for what your app actually needs.
If you build with Lovable, its built-in security scan also looks at this when you publish. It’s worth running, and so are the checks below, since a table can pass “RLS is on” and still be wide open.
3. Read your policies, not just the toggle
RLS being enabled doesn’t mean much on its own. A policy decides who gets which rows, and a policy can let everyone through. List all of yours:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;
Things to look for:
qualis justtrue. That policy matches every row for every caller it applies to. Fine for truly public content like blog posts, bad for anything personal.- Private tables with policies for
anonorpublic. Signed-out visitors shouldn’t be able to read profiles, orders or messages. - Insert or update policies where
with_checkis justtrue. Then a signed-in user can write rows with someone else’suser_id, or move a row to another user. - Roles read from
user_metadata. Users can edit their own user metadata, so “is admin” checks shouldn’t depend on it. Useapp_metadataor a table only your server can write.
The usual pattern for “users only see their own rows” looks like this:
create policy "Users read their own rows"
on public.notes for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users add their own rows"
on public.notes for insert
to authenticated
with check ((select auth.uid()) = user_id);
If you’d rather have your AI builder do it, a prompt like this works well: “Review the RLS policies on every table. Signed-out users should only read public data, and signed-in users should only read and change their own rows. Show me each policy before and after.”
4. Check grants, views and functions
Three things can expose data even when your table policies look right:
- Grants. On many existing projects, new tables in
publicstart with select, insert, update and delete granted toanonandauthenticated. Policies limit which rows, but the grant decides whether the operation is allowed at all. If signed-out visitors never need to write to a table, revoke it:revoke insert, update, delete on public.your_table from anon; - Views. A view in
publicruns with its creator’s permissions by default, which usually means it ignores RLS on the tables underneath. Supabase’s fix (Postgres 15 and up) isalter view public.your_view set (security_invoker = true); - Database functions. Functions in
publiccan be called through/rest/v1/rpc/. A function markedsecurity definerruns with its owner’s rights, so if it returns user data or an API key, anyone who can call it gets that too. Revoke execute fromanon(and frompublic, which every role inherits) for anything signed-out visitors don’t need.
5. Run the logged-out test
This is the test that matters most, because it’s exactly what an attacker does. Take your project URL and public key (from step 1) and ask for a row from each table, with no user signed in:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=1" \
-H "apikey: YOUR_PUBLISHABLE_KEY"
If you’re still on the legacy anon key, also add -H "Authorization: Bearer YOUR_ANON_KEY". A private table should come back as [] or a permission error. If it returns a row, anyone on the internet can do what you just did.
Then do the two-account test: sign up as two different users, and while signed in as the first, try to read or edit the second user’s rows (for example by changing an id in a request). You should get nothing back.
6. Lock down storage buckets
- A public bucket means anyone with a file’s URL can download it. Use public buckets only for things like avatars or marketing images.
- Put invoices, uploads and anything personal in a private bucket, and serve files with signed URLs.
- Storage access is controlled by policies on
storage.objects. A common pattern is one folder per user, with a policy like(storage.foldername(name))[1] = (select auth.uid())::text.
7. Look for other secrets in your bundle
Anything your frontend can read, your visitors can read too. In Vite apps (which includes most Lovable and Bolt projects), every environment variable starting with VITE_ gets copied into the JavaScript that ships to the browser. In Next.js, the same goes for NEXT_PUBLIC_.
That’s fine for your Supabase URL, publishable key, or a Stripe pk_live_ key. It’s not fine for OpenAI, Anthropic or other AI keys, Stripe sk_live_ keys, Resend or SendGrid keys, or anything else that costs money or sends email. Those belong in a Supabase Edge Function or another server-side function, and your frontend calls that function instead.
grep -rlE "sk_live_|sk-proj-|sk-ant-|sb_secret_|AKIA[0-9A-Z]{16}" dist/
8. Check for exposed files and source maps
Some hosts will happily serve files that were never meant to be public. Check these against your live domain:
curl -sI https://your-app.com/.env
curl -sI https://your-app.com/.git/config
A 200 isn’t always a problem, because many single-page apps return the home page for any path. Open the URL in your browser. If you see your app, you’re fine. If you see KEY=value lines or a [core] section, take it down and rotate everything in it.
Also check for public source maps. If your JavaScript files end with a sourceMappingURL comment and the .map file loads, anyone can read your original source code. Vite and Next.js don’t publish browser source maps in production by default, so if you have them, someone turned them on.
While you’re there, check your security headers too: Strict-Transport-Security, a Content-Security-Policy, X-Frame-Options (or CSP frame-ancestors), X-Content-Type-Options: nosniff and a Referrer-Policy. They won’t fix a leaked key, but they close off common attacks for very little effort.
9. If a secret leaked
Rotate first, clean up second. Deleting the key from your code doesn’t help if someone already copied it. Assume they did.
- Move the code that needs the secret to a server or Edge Function.
- Create a new key and deploy it there.
- Revoke or delete the old key in the provider’s dashboard. For Supabase, that’s Settings, then API Keys.
- Check the provider’s usage or billing page for anything you don’t recognize.
- If the key was ever committed to git, it’s still in your history even after you delete the line, so treat it as public forever.
10. What our free scan checks, and what it doesn’t
The LockdownLaunch free scan looks at your live URL from the outside, the same way a browser does. It checks:
- HTTPS, security headers, cookie flags, and whether CORS trusts any website
- Your HTML and same-origin JavaScript for known secret key patterns, including
sb_secret_keys and legacyservice_rolekeys (keys are masked and never used) - Whether
/.envor/.git/configis public, and whether the source maps your JavaScript points to are downloadable - Whether your app uses Supabase or Firebase
It doesn’t touch your database, so it can’t tell you whether your RLS policies actually work. That’s what steps 2 to 6 are for. It also doesn’t see your server code, your repo, or anything behind a login, and a clean result isn’t a security guarantee.
The $49 Launch Report goes one step further once you verify that you own the app: it checks whether your Supabase tables can be read with your public key (it only reads row counts, never row contents), whether storage buckets can be listed, and the equivalent Firebase rules. It isn’t open yet. You can get an email when it opens.
Check your live app in about a minute
Free, read-only, and no signup. It won’t replace the checks above, but it looks for the leaked keys and exposed files that are easy to miss.
Run the free scanSources: Supabase docs on API keys, Row Level Security, Database Advisors and Storage access control; Vite docs on env variables; Lovable docs on security.