That's your Supabase anon key. It's a JWT token that grants "anonymous" access to your Supabase project. And yes, it's supposed to be in your frontend code -- Supabase designed it this way.
But here's what Supabase's documentation buries in a section most developers skip: the anon key is only safe when Row Level Security (RLS) is properly configured on every single table. Without RLS, that public key is an all-access pass to your entire database.
This is the single most common vulnerability we find when scanning apps built with Supabase and AI coding tools like Lovable, Cursor, and Bolt. 76% of the Supabase-powered apps we've scanned have inadequate RLS policies.
How the Anon Key Works
When you create a Supabase project, you get two keys:
Anon key: A JWT with the anon role. This is the public key meant for client-side use. It can access your database, but only through the permissions you define via RLS policies.
Service role key: A JWT with the service_role that bypasses all RLS policies. This key should never be in client-side code -- it's for server-side use only (Edge Functions, backend APIs).
The Supabase client library in your frontend uses the anon key to make API requests directly to your PostgreSQL database through Supabase's PostgREST API. This is what makes Supabase so fast to develop with -- no backend server needed.
But this architecture means that anyone who views your page source, inspects network requests, or looks at your JavaScript bundle can find your anon key and your Supabase project URL. With those two pieces of information, they can make the exact same API calls your frontend makes.
What an Attacker Can Do With Your Anon Key
Let's say your app has a profiles table with columns id, email, full_name, and avatar_url. If you haven't configured RLS on this table, here's what an attacker can do from their browser console:
// Using the anon key they found in your bundle
const supabase = createClient('https://your-project.supabase.co', 'your-anon-key');
// Read ALL user profiles
const { data } = await supabase.from('profiles').select('*');
console.log(data); // Every user's email, name, etc.
// Insert fake data
await supabase.from('profiles').insert({ email: 'attacker@evil.com', full_name: 'Fake User' });
// Delete records
await supabase.from('profiles').delete().neq('id', '');
// Update other users' data
await supabase.from('profiles').update({ full_name: 'Hacked' }).eq('id', 'some-user-id');
This isn't a theoretical attack. We've tested this against real production apps (with permission) and successfully read user data in minutes. The tools required are: a web browser, the developer console, and the ability to copy-paste.
How to Check If Your App Is Vulnerable
Step 1: Find Your Anon Key
Open your deployed app in Chrome, press Cmd+Option+J (Mac) or Ctrl+Shift+J (Windows) to open the console, and run:
// Search the page's JavaScript for Supabase URLs
document.querySelectorAll('script').forEach(s => {
if (s.src) fetch(s.src).then(r => r.text()).then(t => {
if (t.includes('supabase.co')) console.log('Found in:', s.src);
});
});
Or simply view source and search for supabase -- you'll find the project URL and anon key in the JavaScript bundle.
Step 2: Check Your RLS Status
Connect to your database and run:
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
Any table where rowsecurity is false is completely open to anyone with your anon key.
Step 3: Check Your RLS Policies
Even if RLS is enabled, you might have overly permissive policies:
SELECT tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';
Look for policies that use true as the condition -- these allow access to everyone, which defeats the purpose of RLS.
How to Fix It: A Step-by-Step Guide
1. Enable RLS on Every Table
-- Enable RLS on all public tables
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
-- Repeat for every table in your public schema
Important: Enabling RLS without adding any policies will block all access through the anon key. This is actually the safest default -- you then explicitly grant the access you need.
2. Add Policies for Common Patterns
User-owns-data pattern (most common):
-- Users can only read their own profile
CREATE POLICY "Users can view own profile"
ON public.profiles FOR SELECT
TO authenticated
USING (auth.uid() = id);
-- Users can update their own profile
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE
TO authenticated
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
Public-read, owner-write pattern (for blog posts, listings):
-- Anyone can read published posts
CREATE POLICY "Public can read published posts"
ON public.posts FOR SELECT
TO anon, authenticated
USING (status = 'published');
-- Only the author can update their posts
CREATE POLICY "Authors can update own posts"
ON public.posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id);
Admin-only pattern (for sensitive data):
-- Only admins can access this table
CREATE POLICY "Admin access only"
ON public.admin_settings FOR ALL
TO authenticated
USING (public.has_role(auth.uid(), 'admin'));
3. Move Sensitive Operations Server-Side
Some operations should never run from the client, even with RLS:
Sending emails: Use Edge Functions with the service role key
Processing payments: Use Edge Functions that verify payment status server-side
Aggregating data across users: Use database functions with SECURITY DEFINER
Admin operations: Use Edge Functions that check the user's role before acting
Example Edge Function for a sensitive operation:
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req) => {
// This uses the service_role key -- bypasses RLS
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Verify the user's identity first
const authHeader = req.headers.get('Authorization')!;
const { data: { user } } = await supabase.auth.getUser(
authHeader.replace('Bearer ', '')
);
if (!user) return new Response('Unauthorized', { status: 401 });
// Now perform the sensitive operation
// ...
});
4. Test Your Policies
After implementing RLS, test from the client to make sure:
Authenticated users can only access their own data
Unauthenticated users (using just the anon key) can only access truly public data
Edge cases are handled: what happens when a user deletes their account? When a table is empty? When a user tries to access data with a revoked session?
// Test from the browser console
const { data, error } = await supabase
.from('profiles')
.select('*');
// This should either return only your own profile
// or return an error if you're not authenticated
console.log(data, error);
Common RLS Mistakes to Avoid
Mistake 1: Using true as the Policy Condition
-- DON'T do this -- it allows everyone to read everything
CREATE POLICY "bad policy" ON public.profiles FOR SELECT USING (true);
Mistake 2: Forgetting INSERT Policies
You enable RLS and add a SELECT policy, but forget INSERT. Now users can read data but can't create new records. Always add policies for SELECT, INSERT, UPDATE, and DELETE as needed.
Mistake 3: Not Distinguishing Roles
-- This policy lets any authenticated user read all profiles
CREATE POLICY "too broad" ON public.profiles FOR SELECT
TO authenticated USING (true);
-- Better: limit to own profile
CREATE POLICY "own profile only" ON public.profiles FOR SELECT
TO authenticated USING (auth.uid() = id);
Mistake 4: RLS on Auth-Related Tables
Never modify RLS on tables in the auth schema. Supabase manages those. Only configure RLS on your public schema tables.
Let ClearAudit Check Your Supabase Security
Manually auditing every table and every policy is tedious and error-prone. ClearAudit's security scan specifically checks for exposed Supabase configurations and generates an AI fix prompt that tells your coding tool exactly which tables need RLS and what policies to create.
Run your free scan to check if your Supabase app is leaking data. It takes about 2 minutes, and you'll know exactly what needs to be fixed.
How secure is your app? Get your free security report -- ClearAudit runs 120+ security checks and 40+ SEO checks in about 2 minutes. No login. No credit card. Get Your Free Report