<?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.us-east-2.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>Supabase RLS Hardening</title>
      <dc:creator>Rakesh Das</dc:creator>
      <pubDate>Fri, 03 Jul 2026 12:48:32 +0000</pubDate>
      <link>https://dev.to/rakesh_das/supabase-rls-hardening-16l0</link>
      <guid>https://dev.to/rakesh_das/supabase-rls-hardening-16l0</guid>
      <description>&lt;p&gt;While auditing RLS policies on NDF.CTG (a live community platform, 200+ users), I found a role-escalation vulnerability — a policy was missing its WITH CHECK clause. Without it, a member could update their own row and silently promote themselves to admin. The USING clause was doing its job on reads, but writes had no equivalent guard. Fixed it, then went back and audited all 9 tables for the same gap. It's the kind of bug that's invisible until someone exploits it — no error, no crash, just a quiet privilege escalation. Now it's part of my checklist for every new RLS policy: never assume USING covers writes.&lt;/p&gt;

</description>
      <category>react</category>
      <category>supabase</category>
      <category>javascript</category>
    </item>
    <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>
