DEV Community

Rakesh Das
Rakesh Das

Posted on

How I hid my Supabase anon key from the browser using Hono on Cloudflare Workers

Most Supabase tutorials end with your anon key sitting in
.env.local, shipped to the browser, visible to anyone
who opens DevTools.

That was my setup too — until I decided to actually harden
the project before deploying.

This is what I changed and why.

The problem

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

The real test is hitting your database directly with the
anon key, no app involved.

curl https://yourproject.supabase.co/rest/v1/members \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"
Enter fullscreen mode Exit fullscreen mode

When I did this, I found gaps I didn't know existed.

What I found

Gap 1 — UPDATE policies without WITH CHECK

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

Fix:

CREATE POLICY "users can update own profile"
ON public.users FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (
  auth.uid() = id 
  AND role = (SELECT role FROM public.users WHERE id = auth.uid())
);
Enter fullscreen mode Exit fullscreen mode

Gap 2 — Members table was publicly readable

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

Gap 3 — Admin policies referencing the wrong table

One of my admin policies used auth.users metadata to
check role instead of public.users. These are different
tables. The metadata check was unreliable. Fixed to always
read from public.users.

The solution — Hono proxy on Cloudflare Workers

After fixing the RLS gaps, I added a second layer: a Hono
app on Cloudflare Workers sitting between the React
frontend and Supabase.

Top comments (0)