PopKoren introduces Rowly, a security scanner for auditing Row Level Security (RLS) policies in Supabase projects. It analyzes policy logic across tables, views, functions, and roles, providing SQL fixes. PeterBuildsSecure suggests improvements, questioning if Rowly checks for FORCE RLS settings and SECURITY DEFINER functions, which are critical for preventing data leaks.
PopKoren introduces Rowly, a free open-source security scanner for auditing Row Level Security policies in Supabase projects. Rowly analyzes policy logic across tables, views, functions, and roles, and provides ready-to-run SQL fixes. The tool is open source and runs without storing connection strings.
PopKoren introduces Rowly, a free open source security scanner for auditing Row Level Security policies in Supabase projects. Rowly analyzes policy logic across tables, views, functions, and roles, providing SQL fixes without storing connection strings. The scanner is open source and runs in memory.
The CDN cookie bleed and the Stripe webhook body re-parse are both easy to miss in App Router tutorials, and they bite production more often than people admit. After those code fixes I would still hit the live app once logged out and once as a second user, because a correct Set-Cookie header does not prove route handlers and table policies block cross-tenant reads. [https://rowly.me](https://rowly.me) automates that outsider check across sign-in flows, API routes, storage and database policies on Supabase-backed apps like yours.
A separate reviewer user is the clean fix, and it also makes the log readable since you can filter auth events by that one account instead of guessing which IP was Google. I would still check that account can only reach demo rows, not just that its password rotates. https://rowly.me is what I use to confirm that kind of boundary from outside a live deploy, sign-in, APIs, storage and DB rules.
Rotating the review password after each update is the right habit. I would also give the reviewer account its own Supabase user with a role that can only reach the demo data, so a leaked or cached credential never touches real rows, and check your auth rate limits are keyed on more than IP since these all arrive from one range. I built https://rowly.me for the outside view of this, it probes a deployed app's sign-in, APIs, storage and DB rules the way a stranger would.
Adding a security angle to the merge point: when the offline queue replays writes, Supabase sees them as ordinary requests, so RLS has to be what rejects an edit to someone else's row, not client checks that passed when the change was queued. Also watch for sync setups that push from a backend worker with the service role key, which skips RLS completely, and for policies that trust a user_id column the client fills in. Once sync ships, https://rowly.me is one way to confirm from outside that sign-in, endpoints, storage and the Postgres policies still refuse what they should.
Adding to this: index whatever column the policy compares against, like user_id, or the (select auth.uid()) trick still ends up scanning the table. The Performance Advisor mentioned in this thread flags both the per-row auth calls and the missing indexes. After tuning, recheck that the faster policy still denies other users, because rewriting RLS for speed is how rules quietly get loosened; https://rowly.me is what I point at the live app to confirm reads, writes, storage and auth endpoints still hold.
[ Removed by Reddit ]
The service worker check is a great first step, I'd add that a stale cached auth session can also leave supabase-js waiting on a refresh that never resolves, so clearing site data is worth a try too. If it only happens after a deploy, the SW cache version not bumping is the usual culprit. Unrelated to the hang, but once it's stable I'd run [https://rowly.me](https://rowly.me) on the live PWA, a tool I built that checks what's exposed from outside (auth, APIs, storage, DB rules).
Agreed on service\_role being the sneaky one, a backend job failing quietly on a new table is way harder to spot than a 401 in the browser. Worth adding a smoke test that hits each new table through the server path right after the migration lands. Once the grants settle I'd also check the live app from outside; [https://rowly.me](https://rowly.me) is a scanner I made that tests what anon can reach across auth, APIs, storage and DB rules.
Running the update as a real signed-in user and reading the row back is the right test, the service key makes everything look green. I'd also add a negative case where user B tries to update user A's row and you assert it's unchanged, since that's the one that actually leaks. For the deployed side I use [https://rowly.me](https://rowly.me), a scanner I built that pokes the live app from outside (auth, APIs, storage, DB rules) to see what a stranger can do.
pg_pgrst_no_exposed_schemas usually means the authenticator role can't see any of the schemas you listed, even though the dashboard shows them as exposed. I'd check whether a recent migration revoked USAGE on schema public from anon/authenticated, or changed the authenticator role's grants, then reload the schema cache. Once it's back up, worth double checking what anon can actually reach since grants got touched; https://rowly.me is a scanner I built that tests the live app from outside for that (auth, APIs, storage, DB rules).
Good tip, and I'd add a CI step that runs the type gen and fails if the file changed, so a stale types file can't get merged at all. The same drift happens with RLS, where the agent adds a table in a migration and the policy never gets written. After each migration ships I run [https://rowly.me](https://rowly.me), a scanner I built, against the live app to see what auth, API, storage and DB access an outsider actually has.
Revoking execute from anon and public is the step most people miss, since a new function in public is callable by any role by default. I'd add a CI check that lists every SECURITY DEFINER function without a pinned search\_path or with public execute and fails the build. For the deployed side, [https://rowly.me](https://rowly.me) is a scanner I built that hits the live app from outside (auth, APIs, storage, DB rules) and catches RPCs that are still reachable anonymously.
We run the same cross-tenant checks against production too, but only as read-only smoke tests with two dedicated canary tenants, so nothing real gets touched and a broken view shows up within a day instead of weeks. Your point about raw\_user\_meta\_data is the one I see bite people most, a separate memberships table the user can't write to is the right fix. On top of the e2e suite I also point [https://rowly.me](https://rowly.me) at the live app, a scanner I built that probes auth, APIs, storage and DB rules from outside like a stranger would.
Nice, the manual route is painful. One thing I'd add to the readiness report is a security diff: confirm RLS is enabled on every migrated table with the same policies, that storage buckets kept their private or public setting, and that the edge function secrets are rotated rather than copied, since the old Lovable Cloud project may still hold them. After the move, a scan from [https://rowly.me](https://rowly.me) (a scanner I built) is a quick way to check the new project from outside across auth, APIs, storage and database rules.
The credits point is the one that bites people new to Supabase, because a broad 'users can update their own row' policy on the profile table covers every column, credits included. Postgres lets you narrow that with column privileges (revoke update on the credits column from the authenticated role) or by moving balances to a separate table that only server-side code can write. To see how it looks from outside once it's deployed, I built [https://rowly.me](https://rowly.me), which tests what a stranger can reach across auth, APIs, storage and RLS.
Indexes for sure, and with only \~50 inactive users I'd also check who is actually generating the reads. Filter the API logs for anon key requests, because a table with a loose select policy can get scraped by bots, and that shows up as Disk IO long before you notice anything else. Realtime subscriptions and cron jobs are the other usual suspects, and if you want to see what's reachable from outside, [https://rowly.me](https://rowly.me) scans the live app across auth, APIs, storage and DB rules.
[ Removed by Reddit ]
[ Removed by Reddit ]
Agree on buckets, the classic one is a bucket marked public for avatars that later starts holding invoices or exports and nobody revisits the flag. Listing objects with the anon key and requesting paths from another user's folder catches most of it. I also point [https://rowly.me](https://rowly.me) at the live URL before launch since it hits storage, auth and API endpoints from outside the way a stranger would.
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
Good clarification that single-part upsert already takes the UPDATE path, so the missing UPDATE policy bites even without multipart. Storage RLS surprises like this are why I also want a stranger-view of the whole deploy, not only the one bucket policy I just edited: auth, APIs, storage, and DB rules. [https://rowly.me](https://rowly.me) scans that full external attack surface.
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
[ Removed by Reddit ]
Silent wrongness is worse than a loud 500. I have had the same class of bug where RLS or a trigger "looked fine" while the data path quietly did the wrong thing for days. Beside reviewing triggers, I still hit the live app from a second session and check auth, APIs, storage, and policies from outside. [https://rowly.me](https://rowly.me) is the scanner I use for that stranger-view pass.
[ Removed by Reddit ]
[ Removed by Reddit ]
For hackathon demos I treat ship day as the real test: anon key misuse, open buckets, weak RLS, and public APIs a stranger can hit. After deploy, scan the full external attack surface, not only the DB rules. I built https://rowly.me for that live-app check on vibe-coded stacks including Supabase when it is in play.
I landed on Supabase for auth plus Postgres without running my own stack. The first annoyance was realizing generated RLS can look correct and still leave a path open once the app is public. After every deploy I scan auth, APIs, storage, and DB rules from the outside. https://rowly.me is the tool I use for that.
Local policy tests are a good start for RLS and schema assumptions. I still treat the deployed app as a separate check: anonymous access, second-user reads, storage exposure, and any API that bypasses the policies you tested in CI. https://rowly.me scans the full external attack surface, including Supabase/Postgres rules when they are in the stack.
Splitting app reads from analytics is the right instinct. While you are drawing those boundaries, also check which roles and RLS policies the dashboard client actually uses in prod. A pooling fix does not help if a stranger can still hit broad APIs or storage. https://rowly.me scans that live external surface on vibe-coded apps.
Agree the authority boundary has to live server-side. An expected-version RPC helps concurrency, but also assume a modified client will try write paths you did not intend. Before you ship multiplayer, probe the live app for auth, APIs, storage, and DB rules a stranger can hit. https://rowly.me does that external attack-surface scan on vibe-coded deploys.
Rate exceeded on a Replit front end often means anonymous traffic or a leaked client path is hammering Auth or the DB. Cap public endpoints, keep the service role server-side, and verify what an outsider can hit without a session. Rowly scans the full external attack surface of a deployed vibe-coded app, including auth, APIs, storage, and DB rules such as Supabase or Postgres when present: https://rowly.me
Two platforms is fine if you keep one clear trust boundary: the Python service holds secrets and the client only talks through authenticated APIs. Just make sure Supabase RLS, storage, and any direct client routes cannot bypass that. Rowly scans the full external attack surface of a deployed vibe-coded app, including auth, APIs, storage, and DB rules such as Supabase or Postgres when present: https://rowly.me
LLMs can spit out a backend fast, but the risky part is usually the deployed boundaries, not the framework choice. Whatever stack you pick, test auth, API authorization, storage exposure, and database rules from an untrusted account. Rowly scans the full external attack surface of a deployed vibe-coded app, including auth, APIs, storage, and DB rules such as Supabase or Postgres when present: https://rowly.me
A WAF helps with noisy scanning, but it does not replace checking what an anonymous client can still reach through your public APIs and storage. RLS matters, and you still want to verify auth paths, bucket policies, and any service-role misuse from outside. Rowly scans the full external attack surface of a deployed vibe-coded app, including auth, APIs, storage, and DB rules such as Supabase or Postgres when present: https://rowly.me
RLS is confusing because it has to be correct at both the policy and application boundaries. A practical audit is to test as anon, normal user, and a second user, then probe storage, RPCs, and server routes separately from table reads. That outside-in pass is what I’m trying to cover with Rowly across auth, APIs, storage, and DB rules: https://rowly.me
The per-project isolation and dropping the OAuth token are good defaults. I’d still test callback handling, anon-key exposure, storage policies, and migration drift from an external user’s perspective, since a strong RLS suite can miss a public endpoint or bucket. I built Rowly to scan the full external attack surface of deployed vibe-coded apps, including auth, APIs, storage, and DB rules: https://rowly.me
Right, separating the server key from the client is the non-negotiable baseline, while encryption and backup exposure cover a different failure mode. I’d also verify the deployed app cannot reach admin-only paths with the public key and that storage and API rules match the intended tenant boundaries. That kind of outside-in check is what Rowly scans: https://rowly.me
Exactly, service_role should stay server-side and a separate backend reduces accidental exposure, but it still deserves a deployment check for leaked envs, reachable admin endpoints, storage buckets, and database policies. Encryption key custody is another layer, not a replacement for testing auth and authorization paths. I’m building Rowly to scan that full external surface: https://rowly.me
Exactly. RLS is a row boundary, but grants and writable columns can still expand what a valid row update changes. I like pairing policy tests with actual API calls under several identities, which is the approach I’m taking in Rowly’s broader scan: https://rowly.me
The key thing here is that RLS correctness can still leave a table-wide grant or writable column as the escape hatch. I’d keep the revoke and grant audit in migrations and test the deployed API with anon, authenticated, and cross-tenant IDs, plus storage paths if present. That broader external pass is what I’m building with Rowly: https://rowly.me
Turnstile on signup and recovery is useful, but rate-limit every auth path and avoid revealing account state. Before launch, test what an unauthenticated session can reach in the app, APIs, storage, and database rules. https://rowly.me checks the full external surface of a deployed vibe-coded app, including Supabase/Postgres when used.
Before App Store review, treat OTP as one slice of the live surface. From a stranger device, poke auth flows, APIs, storage, and DB rules (Supabase RLS if that is your backend). I built https://rowly.me for that full external attack-surface pass on vibe-coded apps.