<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cuarteromario5-commits</title>
    <description>The latest articles on DEV Community by cuarteromario5-commits (@cuarteromario5commits).</description>
    <link>https://dev.to/cuarteromario5commits</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137137%2F0b35f5fe-6740-4195-905b-415ff86b633a.png</url>
      <title>DEV Community: cuarteromario5-commits</title>
      <link>https://dev.to/cuarteromario5commits</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cuarteromario5commits"/>
    <language>en</language>
    <item>
      <title>A read-only SQL check for the 3 Supabase mistakes AI app builders leave behind</title>
      <dc:creator>cuarteromario5-commits</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:56:37 +0000</pubDate>
      <link>https://dev.to/cuarteromario5commits/a-read-only-sql-check-for-the-3-supabase-mistakes-ai-app-builders-leave-behind-1jha</link>
      <guid>https://dev.to/cuarteromario5commits/a-read-only-sql-check-for-the-3-supabase-mistakes-ai-app-builders-leave-behind-1jha</guid>
      <description>&lt;p&gt;If you built your app with Lovable, Bolt, Cursor or another AI builder on top of Supabase, the database layer is where things quietly go wrong. Nothing errors, the app "works", and the data is readable by anyone who opens DevTools.&lt;/p&gt;

&lt;p&gt;Here's a single &lt;strong&gt;read-only&lt;/strong&gt; query you can paste into &lt;strong&gt;Supabase → SQL Editor → Run&lt;/strong&gt;. It changes nothing. Every row it returns is a real problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. Tables without Row Level Security&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'CRITICAL: table without RLS'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'alter table public.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' enable row level security; -- then add per-user policies'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fix&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt;

  &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
  &lt;span class="c1"&gt;-- 2. Policies that are just "true"&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'HIGH: policy open to everyone ('&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;')'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'replace true with a real condition, e.g. (auth.uid() = user_id)'&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
  &lt;span class="c1"&gt;-- 3. Write policies without a SELECT policy&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'HIGH: can write but cannot read'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'create policy "read own" on public.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' for select using (auth.uid() = user_id);'&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;
  &lt;span class="k"&gt;having&lt;/span&gt; &lt;span class="n"&gt;bool_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INSERT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DELETE'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;bool_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ALL'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Tables without RLS
&lt;/h2&gt;

&lt;p&gt;Your Supabase &lt;strong&gt;anon key is public&lt;/strong&gt; because it ships in your frontend JavaScript. That's fine when RLS is on. With RLS off, anyone holding that key can read and modify the whole table through the REST API.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. It's the pattern behind &lt;strong&gt;CVE-2025-48757&lt;/strong&gt;: in a scan of 1,645 Lovable-built apps, 170 had exposed databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Policies that are just &lt;code&gt;true&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These usually come from a debugging session: "the insert fails, make the error go away". RLS &lt;em&gt;looks&lt;/em&gt; enabled in the dashboard, but a &lt;code&gt;using (true)&lt;/code&gt; policy lets everyone through. For truly public read-only data (a product catalog, say) a &lt;code&gt;SELECT … using (true)&lt;/code&gt; can be intentional. On anything else, or on write policies, it almost never is.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write policies without a SELECT policy (the confusing one)
&lt;/h2&gt;

&lt;p&gt;Symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inserts fail with &lt;code&gt;42501 new row violates row-level security policy for table …&lt;/code&gt; &lt;strong&gt;even though your INSERT policy is correct&lt;/strong&gt;, sometimes even when it's literally &lt;code&gt;with check (true)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"It works in the SQL Editor but not from the app."&lt;/li&gt;
&lt;li&gt;Deletes or updates return success but affect &lt;strong&gt;0 rows&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why: supabase-js runs &lt;code&gt;INSERT … RETURNING&lt;/code&gt; by default, so Postgres has to let you &lt;em&gt;see&lt;/em&gt; the row you just inserted, and that is governed by the SELECT policy. With no SELECT policy, the insert is rejected. Filtered &lt;code&gt;DELETE&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt; can only touch rows that are visible through SELECT, so they silently match nothing. The SQL Editor query has no &lt;code&gt;RETURNING&lt;/code&gt;, which is why it works there.&lt;/p&gt;

&lt;p&gt;I found this exact root cause in three separate threads in the Supabase GitHub discussions in September alone (#50483, #50265, #49801). A variant is a SELECT policy that queries its own table and can't see the row created in the same statement. Use a direct column check such as &lt;code&gt;auth.uid() = user_id&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with the results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No rows:&lt;/strong&gt; 🎉 these three are covered. This check doesn't cover everything: policies can still be &lt;em&gt;logically&lt;/em&gt; wrong, and service-role keys can still leak into the frontend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows you're not sure how to fix:&lt;/strong&gt; each row includes a suggested fix. Test in a branch or a staging project first, never straight in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The query is also in this repo, alongside a worked example of rescuing a broken AI-built app (9 bugs, a test for each, before/after): &lt;a href="https://github.com/cuarteromario5-commits/citafacil-rescue/blob/main/extras/supabase-check.sql" rel="noopener noreferrer"&gt;citafacil-rescue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I fix apps built with AI tools that work in preview but break in production. If you ran this and something came up, feel free to comment with the table names (never data).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
