<?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: LeakPatrol</title>
    <description>The latest articles on DEV Community by LeakPatrol (@leakpatrol).</description>
    <link>https://dev.to/leakpatrol</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%2F4192181%2Fd6874922-d45c-4a06-8094-57e9731cc3f7.png</url>
      <title>DEV Community: LeakPatrol</title>
      <link>https://dev.to/leakpatrol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leakpatrol"/>
    <language>en</language>
    <item>
      <title>Supabase "permission denied for table" since 30 October? Don't let your AI fix it with GRANT ALL</title>
      <dc:creator>LeakPatrol</dc:creator>
      <pubDate>Wed, 04 Nov 2026 21:10:14 +0000</pubDate>
      <link>https://dev.to/leakpatrol/supabase-permission-denied-for-table-since-30-october-dont-let-your-ai-fix-it-with-grant-all-iol</link>
      <guid>https://dev.to/leakpatrol/supabase-permission-denied-for-table-since-30-october-dont-let-your-ai-fix-it-with-grant-all-iol</guid>
      <description>&lt;p&gt;Since 30 October 2026, new Supabase tables aren't granted to the Data API automatically. So the first time your app touches a new table, supabase-js returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code: "42501"
message: "permission denied for table notes"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not a bug. Your app talks to Supabase as &lt;code&gt;anon&lt;/code&gt; (logged out) or &lt;code&gt;authenticated&lt;/code&gt; (signed in), and a role needs a Postgres &lt;code&gt;GRANT&lt;/code&gt; before it can touch a table at all. Older tables got those grants for free. New ones don't.&lt;/p&gt;

&lt;p&gt;The fix is one line. The problem is which line your AI tool picks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two errors, same code
&lt;/h2&gt;

&lt;p&gt;Both of these come back as &lt;code&gt;42501&lt;/code&gt;, but they mean different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What's wrong&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permission denied for table …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No grant for that role&lt;/td&gt;
&lt;td&gt;Add a narrow &lt;code&gt;GRANT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new row violates row-level security policy …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RLS blocked this row&lt;/td&gt;
&lt;td&gt;Add the missing policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you paste either one into Lovable, Bolt or Cursor, check which one it is first. They need different fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safe fix for "permission denied"
&lt;/h2&gt;

&lt;p&gt;RLS and a policy first, then grant only what the app needs, only to the role that needs it:&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;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="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users manage 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;all&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="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="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;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&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;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For data everyone may read but nobody may change from the browser (products, blog posts):&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;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;products&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="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Anyone can read products"&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;products&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;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&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;products&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The fixes that make the error go away and open your table
&lt;/h2&gt;

&lt;p&gt;Your anon key is in your app's JavaScript, so anyone can use it. Watch for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;grant all on public.notes to anon&lt;/code&gt;&lt;/strong&gt;: logged-out visitors get insert, update, delete and &lt;code&gt;TRUNCATE&lt;/code&gt;, which RLS doesn't cover at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granting to &lt;code&gt;anon&lt;/code&gt; on user data.&lt;/strong&gt; Logged-out visitors never need your users' rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granting before RLS is on.&lt;/strong&gt; Until the policy exists, every row is public. If the AI forgets the policy, it stays that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;disable row level security&lt;/code&gt;&lt;/strong&gt; or &lt;code&gt;using (true)&lt;/code&gt; on user data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The service_role key in the frontend.&lt;/strong&gt; It skips RLS completely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A prompt that works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My Supabase table is giving error 42501 "permission denied for table".
Fix it safely:
1. Enable Row Level Security on the table first, with policies so users
   only read and change their own rows (auth.uid() = user_id), including
   WITH CHECK on insert and update.
2. Then grant only the needed privileges (select, insert, update, delete),
   only to the authenticated role. Do not grant to anon unless the data is
   meant to be public, and never use GRANT ALL.
3. Do not disable RLS, do not use using (true) on user data, and do not
   use the service_role key in the frontend.
Show me the SQL before running it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line matters most. Read the SQL before you click run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your old tables didn't change
&lt;/h2&gt;

&lt;p&gt;The 30 October change only affects new tables. A table that was readable with your anon key last week still is. Two queries to check:&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="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'anon'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;table_schema&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="k"&gt;table_name&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; that &lt;code&gt;anon&lt;/code&gt; can select from is readable by anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a live app
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.getleakpatrol.com/?ref=devto" rel="noopener noreferrer"&gt;LeakPatrol&lt;/a&gt;, a free read-only scanner for apps built with Lovable, Bolt, v0 and Cursor. It checks your live site from the outside for leaked keys and public files in about a minute. There's also a free &lt;a href="https://www.getleakpatrol.com/supabase-rls-policy-generator.html?ref=devto" rel="noopener noreferrer"&gt;RLS policy generator&lt;/a&gt; that writes the grants and policies for common table types.&lt;/p&gt;

&lt;p&gt;Full guide with an FAQ: &lt;a href="https://www.getleakpatrol.com/supabase-42501-permission-denied.html?ref=devto" rel="noopener noreferrer"&gt;Supabase error 42501, the safe fix&lt;/a&gt;. And if you're seeing the other one, here's &lt;a href="https://www.getleakpatrol.com/supabase-new-row-violates-row-level-security-policy.html?ref=devto" rel="noopener noreferrer"&gt;the RLS error guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Supabase users can probably give themselves your paid plan. Here's the 30-second test</title>
      <dc:creator>LeakPatrol</dc:creator>
      <pubDate>Sun, 25 Oct 2026 20:52:33 +0000</pubDate>
      <link>https://dev.to/leakpatrol/your-supabase-users-can-probably-give-themselves-your-paid-plan-heres-the-30-second-test-7gl</link>
      <guid>https://dev.to/leakpatrol/your-supabase-users-can-probably-give-themselves-your-paid-plan-heres-the-30-second-test-7gl</guid>
      <description>&lt;p&gt;A lot of apps built with Lovable, Bolt or Cursor handle "upgrade to Pro" with one line in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;is_pro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that line works for your code, it works for anyone. Open DevTools, run the same thing in the console, and the paid plan is theirs. Same for &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;credits&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;trial_ends_at&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RLS doesn't stop it
&lt;/h2&gt;

&lt;p&gt;The usual policy on a profiles table is "users can update their own row":&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;"Users can update own profile"&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;profiles&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;using&lt;/span&gt; &lt;span class="p"&gt;(&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That policy decides &lt;strong&gt;which rows&lt;/strong&gt; a user may change. It says nothing about &lt;strong&gt;which columns&lt;/strong&gt;. If billing columns live on that row, the user can change those too.&lt;/p&gt;

&lt;p&gt;Deleting the &lt;code&gt;update&lt;/code&gt; call from your frontend doesn't fix it either. The policy still allows the request, and anyone can write it by hand with your public anon key and their own login.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second test (on your own app)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Log in as a normal free user.&lt;/li&gt;
&lt;li&gt;Open DevTools → Console and run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;is_pro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Reload. If you're Pro now, so is anyone who tries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use your own table and column names. Only test apps you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: grant updates per column
&lt;/h2&gt;

&lt;p&gt;Postgres can grant &lt;code&gt;UPDATE&lt;/code&gt; on specific columns. RLS still picks the rows; the grant picks the columns.&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;revoke&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;on&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;profiles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&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;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List every column users should be able to edit. If users create their own profile row, do the same for &lt;code&gt;insert&lt;/code&gt;, or someone can sign up with &lt;code&gt;is_pro = true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: a trigger that guards billing columns
&lt;/h2&gt;

&lt;p&gt;If you'd rather not touch grants:&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;protect_billing_columns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_pro&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_pro&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="s1"&gt;'billing columns can only be changed by the server'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;protect_billing&lt;/span&gt; &lt;span class="k"&gt;before&lt;/span&gt; &lt;span class="k"&gt;update&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;profiles&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;function&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;protect_billing_columns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 3 (cleanest): billing gets its own table
&lt;/h2&gt;

&lt;p&gt;Put subscriptions or credits in a separate table with a &lt;code&gt;select&lt;/code&gt; policy for the owner and &lt;strong&gt;no&lt;/strong&gt; insert, update or delete policies. Only your payment webhook writes to it, from an Edge Function using the service_role key, which stays a server secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only the webhook sets the plan
&lt;/h2&gt;

&lt;p&gt;Your Stripe or Lemon Squeezy webhook should verify the signature and then set the plan on the server. Don't set it from the checkout "thank you" page: anyone can open that URL without paying.&lt;/p&gt;

&lt;p&gt;If you build with an AI tool, this prompt works well in Lovable, Bolt or Cursor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Users must not be able to change is_pro, plan or credits. Only the payment webhook, in an Edge Function that verifies the signature and uses the service role key, may set them. Remove all frontend code that writes these columns, revoke UPDATE on profiles from authenticated and grant it back only for the columns users may edit. Do not turn off RLS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last sentence matters. When the new rules break something, AI tools like to "fix" it by disabling RLS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a live app
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.getleakpatrol.com/?ref=devto" rel="noopener noreferrer"&gt;LeakPatrol&lt;/a&gt;, a free scanner for AI-built apps. It now reads the JavaScript your live site loads and flags code that writes paid columns (&lt;code&gt;is_pro&lt;/code&gt;, &lt;code&gt;premium&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;credits&lt;/code&gt;…) straight from the browser, along with leaked keys and readable Supabase tables. It can't see your policies, so run the console test above as well.&lt;/p&gt;

&lt;p&gt;The full guide, with an FAQ, is on &lt;a href="https://www.getleakpatrol.com/supabase-paid-features-unlocked.html?ref=devto" rel="noopener noreferrer"&gt;our site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Are VITE_ environment variables secret? No, and here's how keys leak from Lovable and Bolt apps</title>
      <dc:creator>LeakPatrol</dc:creator>
      <pubDate>Sat, 24 Oct 2026 08:38:25 +0000</pubDate>
      <link>https://dev.to/leakpatrol/are-vite-environment-variables-secret-no-and-heres-how-keys-leak-from-lovable-and-bolt-apps-3io1</link>
      <guid>https://dev.to/leakpatrol/are-vite-environment-variables-secret-no-and-heres-how-keys-leak-from-lovable-and-bolt-apps-3io1</guid>
      <description>&lt;p&gt;Short answer: &lt;strong&gt;no.&lt;/strong&gt; In a Vite app, every environment variable that starts with &lt;code&gt;VITE_&lt;/code&gt; is written into your JavaScript bundle at build time. That bundle is downloaded by every visitor, so anyone can read the value in DevTools.&lt;/p&gt;

&lt;p&gt;The same is true of &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; in Next.js and &lt;code&gt;REACT_APP_&lt;/code&gt; in Create React App.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the prefix exists
&lt;/h2&gt;

&lt;p&gt;Vite hides variables &lt;em&gt;without&lt;/em&gt; the &lt;code&gt;VITE_&lt;/code&gt; prefix from your frontend code on purpose. The prefix is how you tell Vite "this value is safe to publish".&lt;/p&gt;

&lt;p&gt;Here's how secret keys usually end up public in AI-built apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The AI tool writes &lt;code&gt;import.meta.env.OPENAI_API_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It's &lt;code&gt;undefined&lt;/code&gt; in the browser, so the feature breaks.&lt;/li&gt;
&lt;li&gt;The "fix" is to rename it to &lt;code&gt;VITE_OPENAI_API_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The feature works, and the key now ships to every visitor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lovable, Bolt and most Cursor-built frontends are Vite apps, so this comes up a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine in a &lt;code&gt;VITE_&lt;/code&gt; variable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your Supabase URL and &lt;strong&gt;anon&lt;/strong&gt; (or &lt;code&gt;sb_publishable_&lt;/code&gt;) key, as long as Row Level Security is on for every table&lt;/li&gt;
&lt;li&gt;Stripe &lt;strong&gt;publishable&lt;/strong&gt; keys (&lt;code&gt;pk_live_&lt;/code&gt;, &lt;code&gt;pk_test_&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Firebase config and Google Maps keys &lt;strong&gt;restricted to your domain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Analytics IDs (PostHog &lt;code&gt;phc_&lt;/code&gt;, Sentry DSNs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Never in a &lt;code&gt;VITE_&lt;/code&gt; variable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI keys: OpenAI (&lt;code&gt;sk-proj-&lt;/code&gt;), Anthropic (&lt;code&gt;sk-ant-&lt;/code&gt;), Groq, Replicate&lt;/li&gt;
&lt;li&gt;Stripe secret keys (&lt;code&gt;sk_live_&lt;/code&gt;) and webhook secrets (&lt;code&gt;whsec_&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The Supabase &lt;strong&gt;service_role&lt;/strong&gt; or &lt;code&gt;sb_secret_&lt;/code&gt; key, which bypasses RLS entirely&lt;/li&gt;
&lt;li&gt;Email API keys (Resend &lt;code&gt;re_&lt;/code&gt;, SendGrid &lt;code&gt;SG.&lt;/code&gt;), AWS keys, database URLs with a password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not sure which kind a key is? I made a &lt;a href="https://www.getleakpatrol.com/api-key-checker.html?ref=devto" rel="noopener noreferrer"&gt;free key checker&lt;/a&gt; that tells you what a key is and whether it's allowed in the browser. It runs entirely client-side, so the key never leaves the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to move a key out of the frontend
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a server-side function&lt;/strong&gt;: a Supabase Edge Function, a Netlify or Vercel function, or an API route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store the key as a server secret&lt;/strong&gt; with no &lt;code&gt;VITE_&lt;/code&gt; prefix, for example &lt;code&gt;supabase secrets set OPENAI_API_KEY=...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call your function from the frontend&lt;/strong&gt; instead of the provider. Check the user is logged in inside the function, so strangers can't use it as a free proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate the old key.&lt;/strong&gt; It's in every copy of your old bundle, including CDN and browser caches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A prompt that works in Lovable, Bolt or Cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Move every call that uses a secret API key out of the frontend into a
Supabase Edge Function that reads the key from Supabase secrets. Remove
the VITE_ variable for it and call the function instead. Only allow
logged-in users to call it. Don't print any keys in your reply.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check what your live site actually ships
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; file isn't what matters. What matters is what ended up in the &lt;strong&gt;built&lt;/strong&gt; JavaScript. Two ways to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your live site → DevTools → &lt;strong&gt;Sources&lt;/strong&gt; → search all files (Cmd/Ctrl+Shift+F) for &lt;code&gt;sk-&lt;/code&gt;, &lt;code&gt;sk_live&lt;/code&gt;, &lt;code&gt;service_role&lt;/code&gt; and &lt;code&gt;sb_secret_&lt;/code&gt;. You should find nothing.&lt;/li&gt;
&lt;li&gt;Or paste your URL into &lt;a href="https://www.getleakpatrol.com/?ref=devto" rel="noopener noreferrer"&gt;LeakPatrol&lt;/a&gt;, a free read-only scanner I built. It reads every script your site loads, lists any secret keys (masked), and also checks for public &lt;code&gt;.env&lt;/code&gt; files, source maps and missing headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The full version of this guide, with a FAQ, is &lt;a href="https://www.getleakpatrol.com/vite-env-variables-secret.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>react</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase's October 30 change: what it fixes for your app's security, and what it doesn't</title>
      <dc:creator>LeakPatrol</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:10:44 +0000</pubDate>
      <link>https://dev.to/leakpatrol/supabases-october-30-change-what-it-fixes-for-your-apps-security-and-what-it-doesnt-4ob0</link>
      <guid>https://dev.to/leakpatrol/supabases-october-30-change-what-it-fixes-for-your-apps-security-and-what-it-doesnt-4ob0</guid>
      <description>&lt;p&gt;On &lt;strong&gt;30 October 2026&lt;/strong&gt;, Supabase changes how new tables are exposed in existing projects. It's a good security change, but it's easy to misread as "my database is now safe". It isn't: tables you already have keep exactly the access they have today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's changing
&lt;/h2&gt;

&lt;p&gt;Until now, every table created in the &lt;code&gt;public&lt;/code&gt; schema was automatically granted to the &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; roles, so the Data API (REST and GraphQL, which supabase-js uses) could read and write it straight away. Row Level Security was the only thing standing in the way.&lt;/p&gt;

&lt;p&gt;From 30 October, &lt;strong&gt;new&lt;/strong&gt; tables in &lt;code&gt;public&lt;/code&gt; won't get those grants automatically. You have to opt each one in with a Postgres &lt;code&gt;GRANT&lt;/code&gt; before your app can reach it through the Data API. New projects have worked this way since May, and the Supabase CLI switched for local development in June. (&lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;Supabase changelog&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't change
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing tables keep their grants.&lt;/strong&gt; If a table is readable by anyone today, it still will be on 31 October.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row Level Security works the same.&lt;/strong&gt; A table with RLS off, or with a &lt;code&gt;using (true)&lt;/code&gt; policy, is just as open as before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct database connections&lt;/strong&gt; (an ORM or server using a connection string) aren't affected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the change stops &lt;em&gt;new&lt;/em&gt; accidents. It doesn't fix the leaks that are already out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What might break
&lt;/h2&gt;

&lt;p&gt;If your app (or your AI tool) creates new tables after 30 October through migrations or the SQL editor, those tables won't be reachable from the browser until you grant access. Expect a permission error from the Data API on a brand-new table.&lt;/p&gt;

&lt;p&gt;The fix is to grant only what the app needs, and to switch RLS on at the same time:&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;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="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&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;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&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 manage 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;all&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only grant to &lt;code&gt;anon&lt;/code&gt; (logged-out visitors) if the table is meant to be public, like a product list, and then usually just &lt;code&gt;select&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for AI tools "fixing" the error the wrong way.&lt;/strong&gt; If you paste a permission error into Lovable, Bolt or Cursor, check it doesn't respond by granting everything to &lt;code&gt;anon&lt;/code&gt; or turning RLS off. That would recreate exactly the problem this change is meant to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before 30 October
&lt;/h2&gt;

&lt;p&gt;Because existing tables keep their access, this is a good moment to audit them. In the SQL editor:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Which tables have RLS off:&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;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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. What logged-out visitors are allowed to do:&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="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'anon'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;table_schema&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="k"&gt;table_name&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 that appears in the second list with RLS off in the first is readable (and often writable) by anyone who has your anon key, which is in your app's JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Policies that let everyone through:&lt;/strong&gt; look for &lt;code&gt;using (true)&lt;/code&gt; in Authentication → Policies.&lt;/p&gt;




&lt;p&gt;If you'd rather not do it by hand, &lt;a href="https://www.getleakpatrol.com/?ref=devto" rel="noopener noreferrer"&gt;LeakPatrol&lt;/a&gt; is a free read-only check for AI-built apps: leaked keys, public &lt;code&gt;.env&lt;/code&gt; files and missing protections, with a plain-English fix and a copy-paste prompt for each.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>database</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Is your Supabase anon key safe in the frontend? Yes. Here's what isn't.</title>
      <dc:creator>LeakPatrol</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:36:43 +0000</pubDate>
      <link>https://dev.to/leakpatrol/is-your-supabase-anon-key-safe-in-the-frontend-yes-heres-what-isnt-5919</link>
      <guid>https://dev.to/leakpatrol/is-your-supabase-anon-key-safe-in-the-frontend-yes-heres-what-isnt-5919</guid>
      <description>&lt;p&gt;If you've built an app with Lovable, Bolt, v0 or Cursor on top of Supabase, you've probably opened DevTools, seen a long &lt;code&gt;eyJ...&lt;/code&gt; key in your JavaScript, and felt your stomach drop.&lt;/p&gt;

&lt;p&gt;Good news first: &lt;strong&gt;that key is supposed to be there.&lt;/strong&gt; Here's what it actually does, and the four things that &lt;em&gt;do&lt;/em&gt; leak data in AI-built Supabase apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anon key is public by design
&lt;/h2&gt;

&lt;p&gt;Supabase gives every project two kinds of key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Starts with&lt;/th&gt;
&lt;th&gt;Where it belongs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;anon / publishable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eyJ...&lt;/code&gt; (role &lt;code&gt;anon&lt;/code&gt;) or &lt;code&gt;sb_publishable_&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The browser. Fine to ship.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;service_role / secret&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eyJ...&lt;/code&gt; (role &lt;code&gt;service_role&lt;/code&gt;) or &lt;code&gt;sb_secret_&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only on a server. Never in the browser.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The anon key just identifies your project. On its own it grants nothing: what someone can do with it is decided entirely by your &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; policies.&lt;/p&gt;

&lt;p&gt;That's also the catch. &lt;strong&gt;The anon key is safe only if RLS is doing its job.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tables with RLS turned off
&lt;/h2&gt;

&lt;p&gt;With RLS off, anyone who copies your anon key out of your JavaScript can read, and often write, the whole table with a single request. No login needed.&lt;/p&gt;

&lt;p&gt;Check every table in the 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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything showing &lt;code&gt;false&lt;/code&gt; is open. Turn it on and add a policy that ties rows to their owner:&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;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;profiles&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="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users read their own profile"&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;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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch out for policies with &lt;code&gt;using (true)&lt;/code&gt;. They turn RLS on but let everyone through, which AI tools sometimes generate to "fix" a permissions error.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The service_role key in the browser
&lt;/h2&gt;

&lt;p&gt;The service_role (or &lt;code&gt;sb_secret_&lt;/code&gt;) key &lt;strong&gt;bypasses RLS completely&lt;/strong&gt;. If it's in your frontend, every table is open no matter how good your policies are.&lt;/p&gt;

&lt;p&gt;AI tools usually put it there by accident: a &lt;code&gt;VITE_SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt; or &lt;code&gt;NEXT_PUBLIC_...&lt;/code&gt; variable, added to make an admin feature work. &lt;strong&gt;Anything prefixed &lt;code&gt;VITE_&lt;/code&gt;, &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; or &lt;code&gt;REACT_APP_&lt;/code&gt; is bundled into the JavaScript every visitor downloads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you find it: rotate the key in Supabase (Settings → API) right away, then move the admin work into an Edge Function that reads the key from Supabase secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Storage buckets
&lt;/h2&gt;

&lt;p&gt;Storage has its own policies, separate from your table RLS, and they're easy to forget. A "public" bucket means anyone with a file's URL can download it. Fine for avatars, not for invoices, ID photos or exports.&lt;/p&gt;

&lt;p&gt;In Storage, check which buckets are public, and give private ones policies on &lt;code&gt;storage.objects&lt;/code&gt;, just like tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Views and functions that skip RLS
&lt;/h2&gt;

&lt;p&gt;Two quieter ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Views&lt;/strong&gt; run with their creator's rights by default, so a view over a protected table can expose it. On Postgres 15+, create them with &lt;code&gt;with (security_invoker = on)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functions marked &lt;code&gt;SECURITY DEFINER&lt;/code&gt;&lt;/strong&gt; also run with their owner's rights. If one is callable through the API, check it only returns what the caller should see.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 2-minute check
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your live app, then DevTools → Sources, and search for &lt;code&gt;service_role&lt;/code&gt;, &lt;code&gt;sb_secret_&lt;/code&gt;, &lt;code&gt;sk-&lt;/code&gt; and &lt;code&gt;sk_live&lt;/code&gt;. You should find none.&lt;/li&gt;
&lt;li&gt;Run the SQL above. Every table should show &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Look for &lt;code&gt;using (true)&lt;/code&gt; in your policies.&lt;/li&gt;
&lt;li&gt;Check which storage buckets are public.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather not do it by hand, I built &lt;a href="https://www.getleakpatrol.com/?ref=devto" rel="noopener noreferrer"&gt;LeakPatrol&lt;/a&gt;, a free read-only check that looks for leaked keys, a public &lt;code&gt;.env&lt;/code&gt; and missing security headers, and, once you prove you own the site, which Supabase tables can be read without logging in. Each finding comes with a prompt you can paste back into your AI tool to fix it.&lt;/p&gt;

&lt;p&gt;The anon key was never the problem. What you've built around it is what counts.&lt;/p&gt;

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