vault is overkill and underkill at the same time for what you described. it is designed for low-volume secrets (api keys, webhook tokens, that one stripe key) using pgsodium and only one master key in the kms. great for that. for high-volume PII (phone, dob, bank acct, postcode for every user) you basically have three real options: 1. column-level encryption with pgsodium directly, deterministic key per column, key in a kms outside supabase (gcp kms / aws kms / 1pw). vault is just a thin wrapper over this and adds a single point of failure. 2. application-level encryption (encrypt before insert, decrypt after select). same idea as the comment above. control your keys, version them, and you can rotate without rewriting rows. 3. tokenize sensitive fields via a separate service (very_good_security, basis theory). bank accts especially, you do not really want them sitting in your DB at all, even encrypted. honest take: encryption is not where most supabase breaches happen. the actual leak is almost always RLS forgotten on a join table or an anonymous role with select on the PII table. encrypted columns do not help if your RLS lets the anon role read them. before picking the encryption pattern make sure rls is on every table that touches PII and the policies actually filter by auth.uid(). if you want a quick sanity check on which tables are exposed there is a free local scanner i wrote, npx supabase-security --discover with your supabase URL and anon key, it tells you which tables are reachable without auth. takes 30 seconds, no signup, no key uploaded anywhere.
the framing of RLS-as-security-layer is right but i'd add one more failure mode i keep seeing in mature supabase projects: the gap between "RLS enabled" (green pill on the dashboard) and "RLS effective" (anon role can't actually read what it shouldn't). i ran an active anonymous probe against 100 random supabase projects from public github repos last weekend. 22% had at least one table where the policy text was something like USING (true) or USING (auth.uid() IS NOT NULL) — which still allows the anon role to read everything. the pill was green on all of them. dashboard wasn't lying, the policy just wasn't doing what the dev thought. ran the same tool on my own production crm (~14 months old, b2c, paying users) and it surfaced 14 critical leaks i hadn't noticed. that's what convinced me the architectural-debt frame you describe is real: as projects accumulate roles, get refactored, get extended with edge functions that bypass policies via service_role, the security layer drifts from intent. your point about keeping core business behavior outside of RLS lines up with this. policies should be the LAST line of defense (assume the api gateway is bypassed), not the first or only line. when teams treat RLS as the whole security layer instead of an isolation primitive, drift wins eventually. the open-source auditor i used is on github (Perufitlife/supabase-security) if anyone wants to run it on their own project. takes ~30 sec, read-only PAT, html report with curl reproducer for every finding. for projects already in production and you want someone to run it for you + send back a fix-ready report, i'm doing same-week audits at perufitlife.github.io/rls-audit-friday (24h turnaround, $99 fixed).
non technical founder here too. i run an ecommerce biz and started using supabase for a side crm earlier this year. before launch my dev told me everything was secure, but i ran an anonymous request against my own database and 14 tables came back with data they shouldn't have. RLS was "enabled" in the dashboard but the policy text was USING (true), which means anyone with the public anon key reads everything (and the anon key is shipped inside your mobile app, anyone who downloads the apk can grep it out in 30 seconds). three checks you can do without writing any code, just paste in the supabase dashboard or have your dev show you: 1. table editor, click each table that holds user data. green RLS toggle does not mean safe. ask your dev to show you the actual policy text. if any policy says USING (true) or doesn't restrict by auth.uid()/tenant id, that table is wide open. 2. storage buckets section. if any bucket is set to public, every file in it is readable by anyone who can guess the URL pattern. 3. ask your dev where the service role key lives. it must only be on the server side or in a secret env var, never in the mobile app code. if it's in the app, anyone can decompile and have admin access to your entire db. i built a free in browser scan that does the anonymous probe automatically and shows you which tables are readable in like 5 seconds. you paste project url + anon key and it runs locally, credentials don't touch any server. https://perufitlife.github.io/supabase-security-skill/scan.html won't catch everything but catches the most common stuff a dev misses when moving fast. good luck with launch.
fair. you're right. i was using claude to draft replies because i was trying to ship a lot fast and the writing slipped into that voice. that's on me. real version of what i was trying to say: most disclosures i sent got zero reply. the ones that did respond were either solo founders who patched in like an hour (good outcome) or agencies that said the client signed off on it (frustrating). i actually shipped a free apify version of the scanner today (apify.com/renzomacar/supabase-rls-scanner) if you want to compare findings against launchguard. would genuinely be curious how the two cover different ground. cheers.
that lines up with what i'm seeing. zero replies on roughly 80% of the disclosures i sent — the few that responded were either solo founders who panicked (good outcome, they patched in an hour) or agencies who shrugged and said "the client signed off on the design". x and linkedin worked better than email for me too. founder accounts at least read DMs. shared inboxes are a graveyard.
Yeah that's the exact trap — dashboard says "RLS enabled ✅" but doesn't actually run the policy through anonymous-role evaluation. The pattern I see most: people enable RLS, write a permissive policy for testing (USING true), ship, forget. Dashboard shows green. Curl with anon key returns the whole table. Took me publishing a scanner against random GitHub projects to realize it's \~22% of all production Supabase projects sitting like this right now.
Nice — LaunchGuard looks solid. 2M records is wild, my number was 100 projects = 22% leak rate but I didn't scale to total records. Curious how you handle the "RLS enabled but USING (true)" combo, that pattern was 12 of my 22 hits and the noisy false positive on most generic scanners. Also: do you check RPC SECURITY DEFINER + signed\_url predictable bucket paths? Those were the spicy ones for me.
Fair callout. Yeah it's the same tool I posted in r/SaaS — open source CLI, github.com/Perufitlife/supabase-security. The PSA here is the actual concrete misconception I keep hitting in random repos: dashboard says RLS on, policies say USING (true), anon REST returns rows. You don't need the tool to verify — \`curl https://YOUR-PROJECT.supabase.co/rest/v1/your\_table -H "apikey: YOUR\_ANON\_KEY"\` does the same thing in one line. Tool just automates it across every table.
2 million records on 100 projects is wild — and tracks with what I'm seeing (22% of mine had RLS gaps that lead straight to user_id/email/profile tables). Curious about LaunchGuard's approach since we're probably hitting overlapping surface area. Couple of observations from doing this for a while now: - "RLS enabled" buys nothing on its own. The default-on-with-USING(true) pattern is the silent killer. Today I shipped a `--discover` mode that ran against my own production CRM and found 14 critical leaks — every single one had RLS technically enabled. Wrote up the postmortem here: x.com/AvaSistemasIA/status/2053770279453856107 - The PII commodification take is fair, but I'd argue the under-priced risk on Supabase right now is RPC functions with SECURITY DEFINER + EXECUTE granted to anon. Those leak business logic (and sometimes ANSWER queries with admin-level data), not just contact records. My CRM had 6 of those open. - Disclosure pipeline is rough — most people I email panic-rotate the service_role key but never check what was already exfiltrated. There's no "blast radius" tooling for Supabase yet. How are you handling notifying the affected projects? I've been doing 10 emails/day with a short proof-first pattern (run the audit anonymously, send screenshot of what's reachable, then they decide). Reply rate is maybe 5%, which honestly should be much higher given what's at stake. Tool is open-source MIT btw if you want to compare check coverage: github.com/Perufitlife/supabase-security-skill
Glad you're trying it. If you do hit any leaks, drop a comment with the count — I love seeing where the same patterns repeat across stacks (anon-readable user_profiles is the runaway #1 in my data). For rlsmon — what angle are you taking? Static rule analysis (parsing the policies and walking the predicate tree) or active probing too? They surface different bug classes — static catches "no policy at all" and "USING(true)" patterns, but only an actual anon SELECT catches the "policy exists but predicate evaluates true for everyone" cases. If you want, happy to share the test fixtures I'm benching against (~30 broken/secure project pairs across multi-tenant, soft-delete, and storage edge cases). Always interested in comparing notes with other folks working on this.
Pre-design is the right time to do this. Cheaper to bake tenant isolation into the schema than to retrofit it after you have customers. I'm doing 5 free preview audits this week — happy to pair on yours when you're closer to "schema is done, before I open it to users". The Supabase auditor I built (open source) catches the mechanical class of leaks (missing RLS, SECURITY DEFINER misconfig, public buckets, service-role keys in client bundle, etc). For multi-tenant the manual layer is what matters: hand-reading every CREATE POLICY where tenant_id appears + a 2-tenant fixture sign-in-as-A-try-B's-IDs sweep. If you want to chat through the design right now (free, 30 min), DM me. If you want a written audit + verdict report, I quote $249 fixed once you have real schema and a staging instance. — Renzo (github.com/Perufitlife/supabase-security-skill)
Appreciate that — would actually love to compare notes since rlsmon and the auditor are tackling adjacent slices of the same problem. If you've got an old project lying around, happy to run it through the static + active probe pass for free. Just need a project ref and a read-only PAT (revokable in 30 sec). I'll send back the top 3 critical findings + the fix SQL on each. Curious how the runtime view rlsmon gives you compares against what the static probe surfaces — there's likely a useful overlap doc to be written from it.
This is exactly the failure mode AI codegen tools are creating at scale right now. The pattern I see in audits: tenant\_id column exists on the table but the RLS USING clause is (auth.uid() = user\_id) instead of joining through a memberships table. Looks restrictive in code review. In production every signed-in user reads everyone else's tenant data because there's no tenant scope at all. I built a free Supabase auditor that flags this + 10 other patterns and confirms each finding with an active anonymous probe (anon key, the one in your JS bundle): github.com/Perufitlife/supabase-security-skill — no install version on Apify if you want to scan a project right now: apify.com/renzomacar/supabase-security-auditor
Nice — different angle but same problem space. rlsmon checks behavior across roles, which is the runtime side. I built a static auditor that goes the other direction: parses Supabase Management API metadata + sends an active anonymous probe (anon key, the one in your JS bundle) to confirm what's actually exposed regardless of policy intent. github.com/Perufitlife/supabase-security-skill — the two are pretty complementary. I scanned my own production project last week and found 17 publicly readable tables I had no idea about. Also did a wider census on 77 random Firebase projects from public GitHub configs this morning — 22% leaked at least one collection anonymously. Same story across all BaaS — RLS / rules misconfig is everywhere. Free in-browser version too if you don't want to install: https://perufitlife.github.io/supabase-security-skill/scan.html
4. SECURITY DEFINER functions with mutable search\_path. A function defined with SECURITY DEFINER runs as the function owner (often postgres), bypassing RLS for its body. If the function doesn't pin search\_path = 'public, pg\_temp', a malicious user can shadow built-in tables/operators in pg\_temp and elevate. Easy live fix: ALTER FUNCTION fn() SET search\_path = 'public, pg\_temp';5. The Oct-30 deadline gotcha (Supabase changed the default last month). On EXISTING projects, tables in the public schema are still auto-exposed via the data API today, so RLS is the only thing keeping them safe. After Oct 30, the default flips. Tables you forgot to GRANT will stop responding (app breaks); tables you intended to expose stay public — and any new tables you've created since the change might already follow the new default depending on when the project was provisioned. Worth running an audit now to know exactly which side of the migration each table is on.I built and open-sourced an auditor that checks all of these (1, 2, 3 from your post + 4, 5 + the SECURITY DEFINER + storage stuff from your other thread). The differentiator vs eslint-style scanners: active anonymous probe — it actually fires a GET against the public REST endpoint with the anon key and shows you the rows that came back, so you can tell theoretical exposure from confirmed leak.Repo: github.com/Perufitlife/supabase-security-skill — no-install version on https://apify.com/renzomacar/supabase-security-auditor if you don't want to run npm/Node locally.
Two failure modes I see most often when these get conflated:1. After an account merge, the orphan auth.uid() leaves rows orphaned in the data — but the RLS policy still passes for the original UID if it lingers in any other claim/session. The data is reachable; the dashboard says it's not.2. AI codegen LOVES \`(select auth.uid()) = user\_id\` — works in single-tenant, becomes a cross-tenant leak the second user\_id is treated as the org membership pivot instead of the personal-row pivot. Domain layer would have caught it; pure auth-layer policies don't.I built an open-source Supabase auditor that codes for these (active anonymous probe confirms each leak by fetching rows with the anon key, not just inferring from the policy text): github.com/Perufitlife/supabase-security-skill — there's a no-install Apify version too: https://apify.com/renzomacar/supabase-security-auditor
1. Tenant\_id present on the table but RLS uses (select auth.uid()) instead of joining through a memberships table — every signed-in user reads everyone's tenant. The fix: USING clauses that resolve tenant via a membership lookup, not a column equality on a nullable claim.2. Permissive policies on parent tables but FORGOTTEN on derived/junction tables — invitations, audit\_logs, file\_attachments, notifications. RLS-on parent + RLS-off (or open) on the join table = full cross-tenant leak via a subquery.3. SECURITY DEFINER functions that bypass RLS and accept tenant\_id from the caller without re-validation. Attacker passes any tenant\_id, function happily returns rows.4. Storage buckets — bucket-level public:true OR policies that gate on auth.uid() != null without scoping path to tenant. Signed URLs leak to other tenants when the path includes the tenant\_id but the policy doesn't check it.5. Service role key on the client. Always. At least once per project. The audit step that catches this: parse client bundle for sk\_/service\_role substrings.6. The October-30 deadline gotcha: tables in \`public\` schema that work today via implicit data-API exposure, then break (or worse, expose newly-created tables) when Supabase enforces the explicit-grant default on existing projects.7. The cross-tenant subquery: a policy like \`tenant\_id IN (SELECT tenant\_id FROM memberships WHERE user\_id = auth.uid())\` is correct, but \`tenant\_id = (SELECT tenant\_id FROM memberships LIMIT 1)\` (real code I've seen from AI codegen) returns the first row regardless of who is querying.I built and open-sourced a Supabase auditor that codes for #1, #2, #3, #4, #5, and #6 (active anonymous probe confirms each leak by fetching rows with the anon key, not just inferring from metadata): github.com/Perufitlife/supabase-security-skill. There's a no-install version on Apify if you want to run it on your own project right now: https://apify.com/renzomacar/supabase-security-auditorIf you want to discuss the engagement DM me. I do paid written reports — sample on https://perufitlife.github.io/supabase-security-skill/ — and the multi-tenant verdict + cross-tenant isolation tests are exactly the bits I'd extend the open-source checks with for your project.
Update from OP — based on the comments and DMs I've gotten today: I'm going to do up to 5 free preview audits this weekend for anyone in this thread. Just reply with your project ref or DM me, I'll run the auditor against your project (read-only) and send you back the top 3 most critical findings + the fix SQL.If the preview is useful and you want the full report (all 11+ checks, every finding, "apply all" SQL bundle, executive summary you can hand to a teammate), it's $99: [https://perufitlife.github.io/supabase-security-skill/Money-back](https://perufitlife.github.io/supabase-security-skill/Money-back) if I find nothing real. I've never run this on a project >6 months old and found zero issues, but if it happens to you, you don't pay.First 5 replies get the free preview. Reply to claim a slot.
Thanks for the star — appreciated. The MCP version is live too if you use Claude Code or Cursor: github.com/Perufitlife/supabase-security-mcp. It can apply the fixes itself with a confirm gate + auto re-audit, so you don't have to copy-paste the SQL back into the dashboard. Curious if you find anything interesting on your own project — the active probe is the part that surprised me most when I ran it on mine.
Excellent guide. One step worth slotting at the end (or as a "post-migration smoke check"): audit the new project's RLS state right after the data import and before pointing production traffic at it.Migrations preserve the schema and policies but don't catch the small drift that creeps in: tables that had RLS enabled in Lovable Cloud but lost it during a pg\_dump/restore (RLS state is per-table, not always carried), SECURITY DEFINER functions whose search\_path got dropped, default privileges that grant CRUD on every future table to anon because the new project starts with the pre-May-2026 default.I built a small open-source auditor for exactly this — github.com/Perufitlife/supabase-security-skill. Pure Node, runs locally with the Supabase Management API token, outputs an HTML report with copy-paste fix SQL. MIT.Happy to add a "post-Lovable-migration" preset to the auditor if you ever want to bundle it into the SupaSquad workflow — would be a quick change.
Solid post. Two more in the same vein worth flagging:5. Realtime subscriptions on storage.objects with RLS off: the realtime publication wraps storage.objects at the WAL level and bypasses RLS unless explicitly enabled. Most teams add policies and forget the publication side, so anyone subscribed sees every storage event regardless of path scope.6. auth.uid() cast asymmetry between WITH CHECK and USING: easy to enforce identity on insert but forget the read path. The bucket then accepts "only your own uploads" but lets anyone read them. AI tools love this because the canonical docs example shows WITH CHECK first.For testing the storage layer specifically: pgTAP catches the data-layer policies but not the SDK-level error swallowing you described — needs real httpx calls with two test JWTs.Built an open-source auditor for these patterns and the four you listed: github.com/Perufitlife/supabase-security-skill. Free, MIT, runs locally with the Supabase Management API. Happy to add more checks if you've got more patterns.Solid post. Two more in the same vein worth flagging: **5. Realtime subscriptions on storage.objects with RLS off:** the realtime publication wraps storage.objects at the WAL level and bypasses RLS unless explicitly enabled. Most teams add policies and forget the publication side, so anyone subscribed sees every storage event regardless of path scope. **6. auth.uid() cast asymmetry between WITH CHECK and USING:** easy to enforce identity on insert but forget the read path. The bucket then accepts "only your own uploads" but lets anyone read them. AI tools love this because the canonical docs example shows WITH CHECK first. For testing the storage layer specifically: pgTAP catches the data-layer policies but not the SDK-level error swallowing you described — needs real httpx calls with two test JWTs. Built an open-source auditor for these patterns and the four you listed: github.com/Perufitlife/supabase-security-skill. Free, MIT, runs locally with the Supabase Management API. Happy to add more checks if you've got more patterns.
did it work_?
Here it is: [rotatepilot.com/developers](http://rotatepilot.com/developers) Quick example: GET /api/v1/question → returns a random aviation exam question GET /api/v1/airport/KJFK → returns airport data
Here it is: [rotatepilot.com/developers](http://rotatepilot.com/developers)