The same Supabase mistake, three times in a row
I kept finding the same bug. Three different apps, and all three had the same root cause: Supabase with Row Level Security turned off. Anon key in the frontend, no policies on the tables, one GET request to dump everything.
About 29,000 records total across the three. User profiles, GPS coordinates, banking details, crypto exchange accounts. All readable by anyone who knew how to open browser dev tools.
Here's what happened.
What Supabase is and why this keeps happening
Supabase is a hosted Postgres database with a REST API bolted on. You sign up, create a project, and it gives you two API keys: an anon key (public, meant for the browser) and a service_role key (admin, meant for the server). The anon key goes in the frontend JavaScript. That's by design.
The catch is Row Level Security. RLS is a Postgres feature that restricts which rows a query can see based on who's asking. Supabase expects you to turn it on and write policies like "users can only read their own rows." If you don't, the anon key has full SELECT access to every row in every public table. The docs mention this. The dashboard warns about it. People skip it anyway.
Every site I tested had the same setup: anon key in the source, no RLS, full read access. The key was right there in the JavaScript bundle or in the page source. One request to /rest/v1/TABLE_NAME?select=* and you get everything.
Site 1: Embers (embers.jayrb.dev)
Embers is an app where people drop anonymous thoughts on a map, tagged with their mood. You walk past a spot and find what someone left there. Hope, heartbreak, venting. Basically a confessional pinned to a map.
Supabase URL: https://bglibqmhuyvonncknltj.supabase.co
The key was in the page source. I pointed it at the REST API and got:
| Table | Records | Contents |
|---|---|---|
| profiles | 4,503 | User IDs, usernames, creation dates |
| embers | 5,167 | Messages, GPS coordinates, ember type, usernames, TikTok links |
| notifications | 0 | Empty |
No authentication was needed beyond the anon key. Paginated the results in batches of 1,000 and had everything in about 30 seconds.
The problem here isn't usernames. The problem is 5,167 geotagged personal messages. People use this app to say things they can't say out loud. Breakup grief, love confessions, family issues. One user posted from coordinates in southern Philippines with a TikTok link and a message about hiding from their partner for 10 years that their parents didn't approve of the relationship. Another posted "Just tell me po if you don't want me anymore ying" from coordinates in Mindanao.
Every message has latitude and longitude accurate to about a meter. Some have TikTok links attached. You can tie a real person to a real location to something they thought was anonymous.
No passwords or emails leaked. Supabase stores auth data (hashes, addresses) in an internal auth.users table that wasn't accessible with the anon key. I checked. The auth admin endpoints returned 401.
Site 2: CryptoXScanner (cryptoxscanner.com)
Crypto trading scanner. You connect exchange accounts, set up Telegram alerts, it watches the markets.
Same story. Anon key in the frontend, no RLS. One GET request:
GET /rest/v1/users?select=*&limit=1000&offset=0
14,021 user records. Every one of them containing:
- Full real names (from Google OAuth)
- Personal email addresses (Gmail, Hotmail, Yahoo, Yandex)
- Auth0 IDs in the format
google-oauth2|NUMERIC_GOOGLE_ID - Telegram user IDs
- MEXC exchange user IDs
- Account creation timestamps
The user base is international. Turkish, Arabic, Russian, Indonesian, Pakistani names. Registration dates from February 2024 through early 2026.
The Auth0 IDs are the interesting part. For Google OAuth users, google-oauth2|104457142488637442024 contains the actual Google user ID. That can be used to look up the person's Google profile. The Telegram IDs let you find someone's Telegram account. The MEXC UIDs link to a centralized exchange.
So for any given user in this dump, you potentially have: real name, personal email, Google profile, Telegram account, and crypto exchange activity. For 14,000 people who trade crypto. That's a phishing operation's dream.
Site 3: Gustave Auto (gustave-auto.com)
French vehicle transport platform. Drivers register as independent contractors, provide banking details, and the platform dispatches car transport jobs to them.
This one was the worst.
Same pattern: anon key, no RLS, full read access. 5,891 contractor records containing:
| Field | What it is |
|---|---|
| name | Full legal name |
| contact_email | Personal email |
| phone | Personal phone number |
| siret | French business registration number |
| banking_details | IBAN and BIC code |
| invoice_details | Full home address with GPS coordinates |
| stripe_id | Stripe customer ID |
To put a number on this: one record contains an IBAN starting FR 5520041..., a BIC code, a full address at 32 Avenue Jean Moulin in Villeneuve-la-Garenne with lat/long down to the building, a phone number, and a Stripe ID. And there are 5,890 more like it.
Almost all contractors are in France, concentrated around Paris, Lyon, Grenoble, Bordeaux, and Toulouse. Some in London. A few scattered further.
With an IBAN, you can attempt to set up fraudulent SEPA direct debits. You need a signed mandate, but forged mandates are a known fraud vector and the IBAN plus the person's full name and home address gets you most of the way there. This is financial data for nearly 6,000 people, sitting behind a key that's in the browser.
GDPR applies here. Full names, home addresses, bank accounts, phone numbers for French contractors, stored with no access controls. Article 32 requires "appropriate technical measures" for data security. This doesn't qualify.
The pattern
All three sites made the same mistake:
- Used Supabase as the backend
- Left the anon key in the frontend (this is normal and expected)
- Did not enable Row Level Security on their tables
- Did not write any policies restricting who can read what
The attack in every case was identical:
# Find the key in the JS source
# Find the Supabase project URL
# Send one request per table:
curl "https://PROJECT.supabase.co/rest/v1/TABLE?select=*&limit=1000&offset=0" \
-H "apikey: ANON_KEY" \
-H "Authorization: Bearer ANON_KEY"
# Increment offset by 1000 until you get empty results
That's it. No exploit code. No authentication bypass. No clever tricks. Just the REST API doing exactly what it was designed to do, reading rows that nobody told it to protect.
Why it keeps happening
Supabase makes the initial setup fast. You create a project, get your keys, start building. RLS is off by default on new tables. The dashboard shows a warning, but it doesn't block you. So people skip it while prototyping and never come back.
The whole security model depends on RLS being configured. If it is, the anon key is safe to be public. If it isn't, the anon key is an unrestricted database reader. No middle ground.
I think some developers also just don't understand what the anon key does. If you come from a background where API keys are secrets, a key in the frontend JavaScript doesn't feel dangerous because "it's just the public key." And that's true. But a public key with full read access to a table with IBANs in it isn't a key problem. It's a configuration problem.
The fix
Same SQL on every site. Takes five minutes.
-- Enable RLS on all public tables
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
-- Basic policy: authenticated users read their own rows
CREATE POLICY "Users read own data"
ON your_table FOR SELECT
USING (auth.uid() = user_id);
-- For tables that need public read (like map pins visible to everyone):
CREATE POLICY "Public read"
ON your_table FOR SELECT
USING (true);
-- but limit what columns are exposed using a view or select list
Then test it. Log out, use the anon key directly with curl, and confirm you can't read other people's rows. Supabase has a "SQL Editor" right in the dashboard. You can check RLS status with:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
If rowsecurity is false, you have a problem.
Numbers
| Site | Records | Most sensitive field | RLS enabled |
|---|---|---|---|
| embers.jayrb.dev | 9,670 | GPS coordinates + personal messages | No |
| cryptoxscanner.com | 14,021 | Email + Telegram ID + MEXC exchange UID | No |
| gustave-auto.com | 5,891 | IBAN + BIC + home address | No |
| Total | 29,582 |
Three different apps. Three different types of sensitive data. One line of SQL would have prevented all of it.