Using Supabase anon key safely — RLS-protected access only
anon key is public BY DESIGN — without RLS it's a skeleton key. Lovable Apr 2026 BOLA showed 10.3% of apps got this wrong.
Supabase anon key ships in your client bundle (intentionally). Without RLS, every table is open to anyone with the URL.
What it is
anon key = anonymous-role JWT used for browser-side requests. Designed to be public; safety depends on RLS policies on every table.
Vulnerable example
-- table without RLS — anon key reads everything
create table orders (id uuid, user_id uuid, total numeric);
-- no alter table enable row level securityFixed example
-- enable RLS + tenant-scoped policy
alter table orders enable row level security;
create policy "users_read_own_in_tenant" on orders for select
using (
auth.uid() = user_id
and tenant_id = (auth.jwt() ->> 'tenant')::uuid
);How Securie catches it
supabase/migrations/0042_orders_rls.sql:14Using Supabase anon key safely
Supabase RLS specialist scans every migration; flags any table with tenant_id but no RLS as critical.
-- enable RLS + tenant-scoped policy
alter table orders enable row level security;
create policy "users_read_own_in_tenant" on orders for select
using (
auth.uid() = user_id
and tenant_id = (auth.jwt() ->> 'tenant')::uuid
);Checklist
- RLS on every table
- Default-deny baseline
- Tenant-scoped policies
- anon key only client-side
- service_role key server-only
FAQ
Why is anon key public?
It identifies the project + delegates auth to Supabase Auth + RLS. Safety = RLS.
Related guides
Row-Level-Security bypass is the most common data leak in vibe-coded apps. Here is exactly how it happens, how attackers find it, and how to fix it in Next.js + Supabase with one policy update.
The service-role key bypasses every RLS policy you wrote. It exists for a reason; it leaks for many reasons. Here is the rule for when to use it, the patterns that leak it, and the recovery playbook when it does.
Storage buckets default-allow read in tutorials. Add RLS policies + signed URLs for downloads.
Server Actions execute server-side but default to no auth. Add session-required guard at the top of every protected action.