<?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: Muhammad Moiz</title>
    <description>The latest articles on DEV Community by Muhammad Moiz (@muhammadmoiz269).</description>
    <link>https://dev.to/muhammadmoiz269</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%2F721685%2Fd35898db-5b22-4a78-907a-44b48476fc0a.jpeg</url>
      <title>DEV Community: Muhammad Moiz</title>
      <link>https://dev.to/muhammadmoiz269</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammadmoiz269"/>
    <language>en</language>
    <item>
      <title>Supabase RLS Explained: The Security Hole in Most AI-Built Apps</title>
      <dc:creator>Muhammad Moiz</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:49:44 +0000</pubDate>
      <link>https://dev.to/muhammadmoiz269/supabase-rls-explained-the-security-hole-in-most-ai-built-apps-3pn0</link>
      <guid>https://dev.to/muhammadmoiz269/supabase-rls-explained-the-security-hole-in-most-ai-built-apps-3pn0</guid>
      <description>&lt;p&gt;If you built your app with Lovable, Bolt or v0, there's a good chance it runs on Supabase. And there's a real chance that anyone on the internet can read your users' data right now.&lt;/p&gt;

&lt;p&gt;Not because Supabase is insecure. Because of one setting that's easy to get wrong: &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post explains what RLS is, the mistakes that leave data exposed, and how to check your own app in five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your Supabase key is public (and why that's fine)
&lt;/h2&gt;

&lt;p&gt;Supabase gives every project an API that talks directly to your Postgres database. Your frontend calls it with the &lt;strong&gt;anon key&lt;/strong&gt; (newer projects call it the &lt;strong&gt;publishable key&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;That key is meant to be public. It ships in your JavaScript bundle, and anyone can open DevTools and copy it.&lt;/p&gt;

&lt;p&gt;So what stops someone from using your key to read every row in your &lt;code&gt;profiles&lt;/code&gt; or &lt;code&gt;orders&lt;/code&gt; table?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only RLS.&lt;/strong&gt; RLS policies are rules inside Postgres that decide which rows each request is allowed to see or change. If RLS is off, the public key is effectively a master key to that table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 mistakes I'd look for first
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. RLS is turned off
&lt;/h3&gt;

&lt;p&gt;Tables created in the Supabase dashboard's Table Editor have RLS enabled by default. Tables created with raw SQL or migrations, which is how many AI tools generate them, often don't.&lt;/p&gt;

&lt;p&gt;With RLS off, this request returns every row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: YOUR_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_ANON_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. RLS is on, but the policy allows everything
&lt;/h3&gt;

&lt;p&gt;When you turn RLS on with no policies, Postgres blocks everything and the app "breaks." The quick fix people (and AI tools) reach for is:&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"allow all"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;using (true)&lt;/code&gt; means "every row, for everyone." RLS is technically on, but it protects nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reading is protected, but writing isn't
&lt;/h3&gt;

&lt;p&gt;Policies have two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;using (...)&lt;/code&gt; decides which existing rows you can &lt;strong&gt;see, update or delete&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;with check (...)&lt;/code&gt; decides what new or changed rows you're &lt;strong&gt;allowed to write&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common gap: a solid &lt;code&gt;select&lt;/code&gt; policy, but an &lt;code&gt;insert&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt; policy without &lt;code&gt;with check&lt;/code&gt;. A user can then write rows with someone else's &lt;code&gt;user_id&lt;/code&gt;, or update a field like &lt;code&gt;role = 'admin'&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The service role key is in the frontend
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;service role key&lt;/strong&gt; (newer projects: &lt;strong&gt;secret key&lt;/strong&gt;) skips RLS completely. It belongs only on a server. If you can find it in your frontend code or &lt;code&gt;.env&lt;/code&gt; files that get bundled, rotate it now.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Policies trust data the user controls
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;user_metadata&lt;/code&gt; (&lt;code&gt;raw_user_meta_data&lt;/code&gt;) can be edited by the user. A policy like "allow if the user's metadata says &lt;code&gt;role = admin&lt;/code&gt;" can be bypassed by the user simply editing their own metadata. Store roles in &lt;code&gt;app_metadata&lt;/code&gt; or a separate table only your server can write.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a correct setup looks like
&lt;/h2&gt;

&lt;p&gt;Here's a typical "users only see their own rows" setup for a &lt;code&gt;notes&lt;/code&gt; table:&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="c1"&gt;-- 1. Turn RLS on&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Read: only your own notes&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can read their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Create: only notes that belong to you&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can create their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Update: only your notes, and you can't hand them to someone else&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can update their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. Delete: only your own notes&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can delete their own notes"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 6. Keep it fast&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrapping &lt;code&gt;auth.uid()&lt;/code&gt; in &lt;code&gt;(select ...)&lt;/code&gt; lets Postgres evaluate it once per query instead of once per row, which Supabase recommends for performance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;to authenticated&lt;/code&gt; means logged-out visitors get nothing, even before the condition runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own app in 5 minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: find tables with RLS off.&lt;/strong&gt; Run this in the Supabase SQL Editor:&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="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&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;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any table with &lt;code&gt;rowsecurity = false&lt;/code&gt; is readable with your public key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: look for "allow all" policies.&lt;/strong&gt;&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="n"&gt;tablename&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="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;qual&lt;/code&gt; or &lt;code&gt;with_check&lt;/code&gt; equal to &lt;code&gt;true&lt;/code&gt;, and for &lt;code&gt;insert&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; policies with an empty &lt;code&gt;with_check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: open the Security Advisor.&lt;/strong&gt; In the Supabase dashboard, the Security Advisor flags tables without RLS and other common issues. Fix everything marked as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: test as a stranger.&lt;/strong&gt; Run the &lt;code&gt;curl&lt;/code&gt; request from mistake #1 against your important tables. If you get real data back without logging in, that table is exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;None of this is specific to AI tools. I worked with the same idea for years with Firebase security rules: the browser can never be trusted, so every permission has to be enforced in the database or on the server. AI builders are great at getting the screens working. They're less reliable at this invisible layer, because a missing policy doesn't show up as a bug until someone finds it.&lt;/p&gt;

&lt;p&gt;If your app handles real user data, it's worth an hour to check before launch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Muhammad Moiz, a full-stack developer who helps founders take AI-built apps to production. If you'd like a second pair of eyes on your Supabase setup, I offer a &lt;a href="https://muhammadmoiz.dev/services/production-audit" rel="noopener noreferrer"&gt;fixed-price production audit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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