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"
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())
);
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)