<?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: Rakesh Das</title>
    <description>The latest articles on DEV Community by Rakesh Das (@rakesh_das).</description>
    <link>https://dev.to/rakesh_das</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1514190%2Ffd251cb3-ec76-43e8-8954-f240c8728163.jpg</url>
      <title>DEV Community: Rakesh Das</title>
      <link>https://dev.to/rakesh_das</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rakesh_das"/>
    <language>en</language>
    <item>
      <title>How I hid my Supabase anon key from the browser using Hono on Cloudflare Workers</title>
      <dc:creator>Rakesh Das</dc:creator>
      <pubDate>Wed, 10 Jun 2026 07:25:11 +0000</pubDate>
      <link>https://dev.to/rakesh_das/how-i-hid-my-supabase-anon-key-from-the-browser-using-hono-on-cloudflare-workers-4h84</link>
      <guid>https://dev.to/rakesh_das/how-i-hid-my-supabase-anon-key-from-the-browser-using-hono-on-cloudflare-workers-4h84</guid>
      <description>&lt;p&gt;Most Supabase tutorials end with your anon key sitting in &lt;br&gt;
&lt;code&gt;.env.local&lt;/code&gt;, shipped to the browser, visible to anyone &lt;br&gt;
who opens DevTools.&lt;/p&gt;

&lt;p&gt;That was my setup too — until I decided to actually harden &lt;br&gt;
the project before deploying.&lt;/p&gt;

&lt;p&gt;This is what I changed and why.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The Supabase anon key is "safe" only if your RLS policies &lt;br&gt;
are airtight. That's a big "if." Most developers (myself &lt;br&gt;
included) write RLS policies and test them through the app &lt;br&gt;
— which only tests the happy path.&lt;/p&gt;

&lt;p&gt;The real test is hitting your database directly with the &lt;br&gt;
anon key, no app involved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://yourproject.supabase.co/rest/v1/members &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;p&gt;When I did this, I found gaps I didn't know existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gap 1 — UPDATE policies without WITH CHECK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A policy can have a USING clause (checks old values) but &lt;br&gt;
no WITH CHECK clause (checks new values). This means a &lt;br&gt;
member could update their own row and change their role &lt;br&gt;
to admin. The policy allows the update because the old &lt;br&gt;
row passes USING — it never checks what the new row &lt;br&gt;
looks like.&lt;/p&gt;

&lt;p&gt;Fix:&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;users&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;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;id&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&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;role&lt;/span&gt; &lt;span class="k"&gt;FROM&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;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gap 2 — Members table was publicly readable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had a policy that let anyone read the members table. &lt;br&gt;
That means names, bios, and join years were exposed to &lt;br&gt;
unauthenticated requests. Dropped that policy. Members &lt;br&gt;
can only read their own row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 3 — Admin policies referencing the wrong table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of my admin policies used &lt;code&gt;auth.users&lt;/code&gt; metadata to &lt;br&gt;
check role instead of &lt;code&gt;public.users&lt;/code&gt;. These are different &lt;br&gt;
tables. The metadata check was unreliable. Fixed to always &lt;br&gt;
read from &lt;code&gt;public.users&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution — Hono proxy on Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;After fixing the RLS gaps, I added a second layer: a Hono &lt;br&gt;
app on Cloudflare Workers sitting between the React &lt;br&gt;
frontend and Supabase.&lt;/p&gt;

</description>
      <category>react</category>
      <category>community</category>
      <category>developers</category>
    </item>
  </channel>
</rss>
