• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog

Apr 6, 2026 · 5 min read

Most AI-built Supabase apps leak their users table. Here's how to check yours in 2 minutes.

Two minutes, one SQL paste, one line of JavaScript. Create a free Supabase project, run the drill, and learn the single most common Supabase mistake in AI-generated code — on a throwaway instance you control.

FFlowpatrol Team·Security
Most AI-built Supabase apps leak their users table. Here's how to check yours in 2 minutes.

TL;DR

The Supabase anon key is meant to be public. That's true, and it keeps getting people's entire users tables dumped from the browser console. Here's the 2-minute drill to see exactly why — and how to fix it — on a throwaway project you spin up yourself.

An attacker silhouette standing in front of a Supabase trust boundary, the anon key glowing in their hand
An attacker silhouette standing in front of a Supabase trust boundary, the anon key glowing in their hand

Spin up a free Supabase project (30 seconds)

Go to supabase.com, click New project, pick any name and password, choose the free tier. Wait for the green dot. That's it — you now have a real Postgres database, a real PostgREST API, and a real anon key, all on a URL you control.

You'll need two things from Settings → API in a minute: your Project URL and your anon public key. Keep that tab open.

Mirror the quickstart (30 seconds)

Open the SQL Editor and paste this in. It's the shape of table every "build a SaaS with Supabase" tutorial produces on day one.

CREATE TABLE users (
  id bigserial PRIMARY KEY,
  email text,
  role text DEFAULT 'member',
  password_hash text
);

INSERT INTO users (email, role, password_hash) VALUES
  ('alice@example.com', 'admin',  '$2b$10$fakehashfakehashfakehashfakehashfakeha'),
  ('bob@example.com',   'member', '$2b$10$fakehashfakehashfakehashfakehashfakehb'),
  ('carol@example.com', 'member', '$2b$10$fakehashfakehashfakehashfakehashfakehc');

Hit Run. Three rows inserted. Notice what you did not do: you did not enable Row Level Security. Neither does the quickstart, and neither does any AI code generator I've watched scaffold a Supabase app. That's the setup. The rest of the drill is about what that single omission costs you.

A diagram contrasting the anon key, the authenticated role, and the service role — showing what each one is allowed to touch when RLS is off versus on
A diagram contrasting the anon key, the authenticated role, and the service role — showing what each one is allowed to touch when RLS is off versus on

Run the exfil (30 seconds)

Open any browser tab — literally any one, a blank about:blank works — pop the DevTools console and paste this. Swap in your Project URL and anon key from the Settings → API tab you left open.

await fetch('https://<your-ref>.supabase.co/rest/v1/users?select=*', {
  headers: {
    apikey: '<your-anon-key>',
    Authorization: 'Bearer <your-anon-key>'
  }
}).then(r => r.json())

Here's what comes back:

[ { "id": 1, "email": "alice@example.com", "role": "admin", "password_hash": "$2b$10$fakehash..." }, { "id": 2, "email": "bob@example.com", "role": "member", "password_hash": "$2b$10$fakehash..." }, { "id": 3, "email": "carol@example.com", "role": "member", "password_hash": "$2b$10$fakehash..." } ]

Every row. Every email. Every password hash. One line of JavaScript, zero authentication, zero cleverness — using a key Supabase's own docs told you to embed in your frontend.

A flow diagram showing the anon key moving from the browser console to PostgREST and back with the full users table
A flow diagram showing the anon key moving from the browser console to PostgREST and back with the full users table

What just happened

The anon key is not a secret — Supabase says so and they're right. But it's still a key, and keys identify you as a role.

  • The anon key is a JWT with role: anon in its claims.
  • When PostgREST receives a request with it, it runs your query as the anon Postgres role.
  • anon is a real Postgres role with real GRANTs. By default, the public schema grants it SELECT on every table you create there.
  • Row Level Security is a separate feature you have to explicitly turn on. It's off by default on every new table.
  • No RLS + default GRANTs = the anon key can read everything.

This is the entire bug

If your Supabase app has any table without RLS enabled, it is readable by anyone with your anon key — which is anyone who has ever opened your site. Not "theoretically", not "under certain conditions", not "if they find an exploit". It is a one-line fetch.

Fix it (30 seconds)

Back to the SQL Editor. Paste this:

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can read their own record"
  ON users FOR SELECT
  USING (auth.uid() = id::text::uuid);

Now re-run the exact same fetch() from the browser console. You should see:

[]

Same key. Same endpoint. Same query. Empty array. That's RLS rewriting the query at the database layer — the client cannot opt out, no matter what it sends in the headers. Drill complete.

Audit your own app

Now the point of the exercise. Open the SQL Editor of your actual production Supabase project and run:

SELECT relname FROM pg_class
WHERE relkind = 'r'
  AND relnamespace = 'public'::regnamespace
  AND relrowsecurity = false;

Every table name that comes back is readable with your anon key. Not maybe. Not sometimes. Readable right now by anyone who has ever loaded your site.

Two more checks worth five minutes

Also confirm that your public schema hasn't been granted INSERT, UPDATE, or DELETE to anon — an open write path is worse than an open read path. And double-check your frontend bundle to make sure you're shipping the anon key, not the service role key. The service role key bypasses RLS entirely.

A before-and-after table-privilege matrix showing the public schema with RLS off versus RLS on, with the anon row going from green checkmarks to red X's
A before-and-after table-privilege matrix showing the public schema with RLS off versus RLS on, with the anon row going from green checkmarks to red X's

Why AI code generators miss this

The Supabase quickstart is the happy path. The happy path does not enable RLS. AI code generators follow the happy path. That's the whole story — no malice, no negligence, just defaults compounding.

Closing

This drill finds one bug on one table. A black-box scanner finds the compounding chain — INSERT grants on the anon role, service-role keys leaked into the bundle, cross-table joins that bypass per-table policies, the dozen other things that stack up in AI-generated Supabase apps. That's what we built Flowpatrol for.

Back to all posts

More in Security

How Your Stripe Webhook Gave Users Two Ways to Go Premium
Apr 6, 2026

How Your Stripe Webhook Gave Users Two Ways to Go Premium

Read more
Admin Panels Wide Open: The Door AI Forgot to Lock
Apr 5, 2026

Admin Panels Wide Open: The Door AI Forgot to Lock

Read more
Your Sign-Up Flow Has a Backdoor
Apr 4, 2026

Your Sign-Up Flow Has a Backdoor

Read more