How to monitor your Supabase app for leaked keys and open RLS
Short answer: check three things, and keep checking them after every change. (1) No secret key is shipped in the JavaScript your site serves. (2) No table or view answers to your public anon key unless it's meant to be public. (3) Something re-runs those checks after every deploy and tells you when the result changes. Below is how each one goes wrong in AI-built apps, the free tools that cover it, and where an outside monitor like LatchDog fits in.
1. How keys end up in your frontend bundle
Lovable, Bolt, v0 and most Cursor/Vite/Next.js setups build a JavaScript bundle that every visitor downloads. Anything compiled into it is public, including:
- Env vars with a browser prefix:
VITE_,NEXT_PUBLIC_,REACT_APP_. PuttingVITE_SUPABASE_SERVICE_ROLE_KEYin.envships it to every visitor. - Keys an AI assistant pasted straight into a component "to make it work", often while debugging a failing call.
- Public source maps (
.js.map), which can contain your original source, including keys that were removed from the minified code but not from the source. .envor.gitfolders accidentally included in the published output.
To check by hand, open your live site, view the loaded scripts in DevTools, and search for service_role, sb_secret_, sk_live_, sk-proj-, sk-ant-. A JWT starting with eyJ can be decoded at any JWT debugger; look at its role claim.
2. anon key vs service_role key: which one is a leak?
| Key | In the browser? | Why |
|---|---|---|
anon JWT or sb_publishable_... | Expected | Designed to be public. It can only do what your Row Level Security (RLS) policies allow. |
service_role JWT or sb_secret_... | Never | Bypasses RLS entirely: full read/write to every table. |
So "my anon key is visible" is not a leak in itself. It becomes one only when a table or view is readable through it. If a service_role or sb_secret_ key leaked, follow Supabase's order: fix the code that exposed it, create a new secret key, deploy it everywhere, confirm nothing uses the old one, then delete it (or deactivate legacy JWT keys). Rotating first and fixing later just leaks the new key.
3. Checking RLS (and the view trap)
- Every table in an exposed schema (normally
public) needs RLS on and policies that say who may read and write. Tables created with SQL or by migrations don't get RLS automatically. - RLS on with
using (true)foranonis the same as RLS off for reading. - Views bypass RLS by default. A view runs with its owner's rights (usually
postgres), socreate view public.profile_list as select * from profilescan expose a table whose own RLS is perfect. On Postgres 15+, usecreate view ... with (security_invoker = true)(oralter view ... set (security_invoker = true)), or move the view out of the exposed schema. - The real test is from the outside: call
/rest/v1/<table>?select=*&limit=1with only the anon key. If rows come back for data that should be private, it's open, whatever the dashboard says.
alter table public.profiles enable row level security; create policy "owner reads own profile" on public.profiles for select to authenticated using ((select auth.uid()) = user_id); alter view public.profile_list set (security_invoker = true);
Changing on October 30, 2026: new tables need an explicit grant
Supabase is changing a default (Supabase changelog). Until now, every new table in public got select, insert, update and delete granted to anon, authenticated and service_role, so it was reachable through the Data API the moment it was created. That default is already off for new projects. From October 30, 2026 it is enforced on existing projects too: a new table in public isn't reachable through the Data API (supabase-js, /rest/v1/, GraphQL) until you grant access. Existing tables keep their current grants. Apps that only use a direct Postgres connection are not affected.
- Grants and RLS are separate layers. A grant decides whether a role can reach the table at all. RLS decides which rows it sees. You still need both.
- The error is 42501 (
permission denied for table ...), and the response hint names theGRANTthat's missing (Supabase: 42501 errors). When your AI tool hits it, have it add the narrowest grant each role needs, together with RLS and policies, in the same migration. Don't let it "fix" the error by turning RLS off or granting everything toanon. - Nothing changes for tables you already have. A table that is open to the anon key today stays open. Supabase says Security Advisor flags affected tables before the change, and the Table Editor shows a Data API exposure badge.
-- Supabase's recommended order for a new table grant select on public.your_table to anon; grant select, insert, update, delete on public.your_table to authenticated; grant select, insert, update, delete on public.your_table to service_role; alter table public.your_table enable row level security; create policy "users can read their own rows" on public.your_table for select to authenticated using (auth.uid() = user_id);
4. Free tools, and when they're enough
- Supabase Security Advisor (Dashboard → Advisors). Free and inside your project: it flags tables with RLS disabled, views that run with their owner's rights (security definer views) and other database-side issues. It sees your schema directly, which an outside scan can't. Run it after every migration.
- Lovable's built-in scans: Quick scan runs on every publish and includes a database review; Deep scan reviews your code. Bolt has a security audit in the Publish menu and a database security check.
- Open-source / free checkers: the
supabase-securitynpm package audits a project for RLS-off tables, public buckets and risky grants. RLS Radar and similar paste-a-URL tools test a live site once.
When that's enough: if you deploy rarely, run the builder's scan on each publish, and run Security Advisor after each migration, you may not need anything else. These tools are free, and the builder and Advisor run where your code and schema live.
What they don't do: watch the deployed site over time. They run when you remember to run them, or on publish for that builder only, and they don't tell you that a key showed up in last Tuesday's bundle, or that a table that was closed last week answers to the anon key today.
5. Monitoring over time
AI edits regress things silently: a prompt to "fix the profile page" can add a view, drop a policy or inline a key. Monitoring means the same outside-in checks on a schedule and after each deploy, compared with the previous result, so you only hear about changes. You can script this yourself (a CI job that greps the built bundle for secret prefixes and runs anon-key limit=1 queries against your tables), or use a service.
What LatchDog does here (and doesn't)
- Free check, no sign-up: headers and HTTPS/TLS only (security headers, HTTPS and the HTTP-to-HTTPS redirect, insecure http:// scripts). It does not look for keys or test your database.
- Deep scan, only after you verify you own the domain (DNS TXT record, meta tag, or
/.well-known/latchdog.txt). Ownership is re-checked before every deep scan. LatchDog loads your homepage and the same-origin JavaScript it references (up to 15 files and up to 3 source maps) and looks forservice_role/sb_secret_keys and other provider secrets. It does not report the anon or publishable key as a leak. It also checks for exposed.env/.gitfiles and permissive CORS. - Supabase: using the anon key found in your own bundle, it sends one
SELECT ... LIMIT 1to up to 15 tables or views listed by your project's API (or referenced by.from("...")in your code) and records only whether a row came back. Row contents are discarded. - Schedule: the free plan includes one deep scan after verification. Starter ($19/month, 1 app) re-scans weekly. Pro ($39/month, up to 5 apps) scans weekly and after every deploy via a deploy webhook (at most one deploy scan per 10 minutes per app). Per-deploy checks are Pro only.
- Alerts: weekly and deploy scans email you only when something is new, worse or fixed since the last scan.
- Limits: it's an outside-in check. It can't see your policies, functions or grants (use Security Advisor for that), can't test what one signed-in user can read of another's data, and can miss tables if your project hides its API listing and your code doesn't reference them. It's not a penetration test.
A sensible setup: Security Advisor after migrations, your builder's scan on publish, and an outside monitor on the live site so regressions between those moments don't go unnoticed.
Check your app now
Free, read-only public check: no sign-up, no card. Headers and HTTPS only. Deep scan (leaked keys, open Supabase tables, .env/.git) after you verify you own the domain.