August 9, 2026
The 2 AM Query That Started Rowly
Why I built an open-source RLS auditor after years of breaking into Supabase apps for a living.
By PopKoren
5 min read
I've spent most of my career as a penetration tester. My job, on paper, is simple: clients hire me to break into their applications before someone else does. In practice, it means I spend a lot of time staring at database permissions, because that's where the real damage lives. Not in some exotic zero-day. In a permission boundary that someone thought was closed, and wasn't.
Somewhere around my fortieth Supabase engagement, I noticed a pattern. Almost every team I tested had heard of Row Level Security. Most of them had turned it on. And a disturbing number of them had gotten it wrong in exactly the same way.
"RLS is on" is not a security control. It's a sentence.
Here's the scene, more or less identical across a dozen different clients: a founder or a solo engineer is shipping fast. They spin up a Supabase project, wire up auth, and at some point remember the golden rule everyone repeats - turn on RLS. So they do. They write a policy. Something like:
-- looks safe. isn't.
CREATE POLICY "Enable read access for all users"
ON public.orders
FOR SELECT
USING (true);-- looks safe. isn't.
CREATE POLICY "Enable read access for all users"
ON public.orders
FOR SELECT
USING (true);It compiles. It works in the demo. The checkbox in the dashboard goes green. Ship it.
Except USING (true) doesn't restrict anything. It's a policy in name only - every row, to every anonymous request, forever. I'd point my scanner (or just Postman) at the anon key, hit /rest/v1/orders, and pull down every customer's order history, address, sometimes wallet addresses and payment metadata, from a database that its owners genuinely believed was locked down.
The worst part wasn't finding it. It was watching smart, careful engineers not find it themselves, because nothing in their workflow ever asked the follow-up question.
RLS is on - but is the policy actually doing anything?
I went looking for a tool that asked that question. It didn't exist.
The obvious place to look was Supabase's own Security Advisor, built into the dashboard. I like the Advisor, it catches the most obvious sin, RLS being off entirely, and it catches tables with no policy at all. That's a real, valuable check.
But it stops right where the interesting bugs begin. Advisor does binary checks: is RLS on, is there a policy present. It has no opinion on whether that policy is a tautology. It doesn't parse your USING clause and notice it's true. It doesn't know that your wallet_address column is more sensitive than your created_at column. It doesn't trace a SECURITY DEFINER function that quietly bypasses RLS three tables away, or notice that your storage bucket policy allows public writes into other users' folders. It's a dashboard widget checking dashboard-shaped problems, running after you've already deployed.
I don't fault Supabase for this, the Advisor was never designed to be a policy-logic engine, and it does a fine job of what it does. But relying on it as your only line of defense is like having a smoke detector and calling it a fire marshal. It tells you something is missing. It doesn't tell you what's actually wrong, how bad it is, or how to fix it, and it definitely doesn't run in your CI pipeline before the broken policy ever reaches production.
So on a flight home from an engagement, the idea, like most good and bad ideas, arrived somewhere over a time zone I don't remember. I started sketching what I actually wanted as a pentester: something that reads pg_policies the way I would , logically, adversarially, and tells a developer in plain language: this policy grants access to everyone, here's the proof, here's the SQL to fix it.
That sketch became Rowly.
What Rowly actually does
Rowly is a security auditor purpose-built for Supabase and Postgres RLS. You point it at a connection string - locally, or as a CI gate - and it reads catalog metadata (never your row data) to answer the question Advisor doesn't.
- It parses policy logic, not just policy presence.
USINGandWITH CHECKclauses get analyzed for tautologies, unrestrictedanon/authenticatedaccess, and missing write checks - the exact bug class that let me walk out of engagements with someone else's customer data. - It understands context, not just catalog rows. A table with a column named
api_keyorwallet_addressand no RLS is treated differently than a table of blog categories. It followsSECURITY DEFINERfunctions, views, and triggers to catch bypasses that don't live in the table you're staring at. - It covers the whole surface, not just tables: Storage bucket policies, Auth config, roles and
BYPASSRLS, cron jobs, Vault secrets, foreign tables, publications - the places I actually found things during real engagements. - It gives you a score and a fix, not just a warning. Every table is rated, and high-confidence findings ship with ready-to-run hardening SQL you can paste straight into the SQL editor.
- It runs before you ship, not after. As a CLI with an
--exit-codeflag, Rowly slots into CI and blocks a merge the moment a permissive anon policy shows up.
One command, no dashboard required:
npm run rowly — -c "$DATABASE_URL" — exit-code
No data leaves your infrastructure. Rowly reads schema metadata over your own connection string and never touches row contents.
Advisor vs. Rowly
What each tool actually checksDimensionSecurity AdvisorRowlyDepthBinary: RLS on/off, policy present/missingParses USING / WITH CHECK logic for tautologies and open accessScopeUI snapshot of common table issuesTables, policies, functions, views, triggers, roles, Storage, AuthContextGeneric Postgres viewFlags sensitive columns, SECURITY DEFINER chains, cascading riskFixAdvises you to lookShips hardening SQL, ready to pasteTimingInside the dashboard, after deployCLI + CI - blocks the merge before productionTrustClosed product rulesMIT, open-core — audit the engine yourself
Why it's open source
This part isn't a marketing decision, it's a trust decision. I spent years telling clients not to trust a security claim they couldn't verify themselves. It would have been hypocritical to ask them to trust my tool on faith.
The core scanner — the policy analyzer, the CLI, the detection logic - is MIT licensed at github.com/rowly-app/core. You can read exactly how a finding is derived, audit it for false positives, extend it with your own rules, or fork it entirely. No auth, no telemetry, no account required to run it. If you don't trust me, you don't have to - you can just read the code.
For teams that want history, saved projects, and scheduled rescans across a fleet of databases, there's an optional hosted layer on top (Rowly Cloud) -the same open-source engine underneath, just with a dashboard and a scheduler wrapped around it. But the thing that actually finds the vulnerability was never behind that paywall, and it never will be.
The point
Supabase gave developers an incredible amount of leverage - a production-grade Postgres database, wired to auth, exposed over a REST API, in an afternoon. RLS is the seatbelt for that leverage. But a seatbelt you've buckled loosely is worse than no seatbelt at all, because it gives you false confidence right up until the moment it doesn't.
I built Rowly because I was tired of being the person who found that out for my clients, after the fact, with an invoice attached. I'd rather it get caught by a CI job at 2 PM on a Tuesday than by someone like me at 2 AM with an anon key and free time.
→ Try it: github.com/rowly-app/core
→ Hosted dashboard: rowly.me
If you run Supabase in production, run npm run rowly -- -c "$DATABASE_URL" once, tonight. It's read-only, it's free, and it takes less time than reading this post did.