DEV Community

Cover image for 16,326 Supabase databases are readable by anyone. Here's how to check yours in two minutes
Ohad Krispin
Ohad Krispin

Posted on

16,326 Supabase databases are readable by anyone. Here's how to check yours in two minutes

UpGuard Research looked at about 300,000 domains that use Supabase and found 16,326 databases exposing tables that anyone could read. Over half had signs of personal data. Some had passwords or authentication tokens.

These weren't abstract records. A US valet service exposed over 100,000 customers, about 78,000 of them with license plate numbers. A Canadian immigration coaching service had 884 passwords stored in plain text. A consulate exposed 25,000 people with their home addresses.

Supabase itself wasn't breached. Every one of these was a configuration problem in the app.

Why this keeps happening with AI-built apps

The line in UpGuard's report that matters most for anyone building with Lovable, Bolt, Cursor or Claude Code:

"Tables created programmatically through the API, which is how coding agents interact with Supabase, do not enable RLS by default."

When you create a table in the Supabase dashboard, row level security is switched on for you. When your coding agent creates it through a migration or the API, it isn't. Your frontend ships the anon key to every visitor, and with RLS off, that key can read and change the whole table through the Data API.

The second pattern is the service role key. It bypasses RLS completely, and agents sometimes put it in a variable like NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY. Anything with NEXT_PUBLIC_ ends up in the browser bundle.

Check 1: in the database (free, 30 seconds)

Open the SQL editor in your Supabase project and run:

select schemaname, tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Enter fullscreen mode Exit fullscreen mode

Every row it returns is a public table without RLS. If any of them hold user data, enable RLS and add policies before anything else:

alter table public.your_table enable row level security;
Enter fullscreen mode Exit fullscreen mode

Then add a policy for who can actually read it. Enabling RLS with no policy blocks everyone, which is safe but will break your app until you add one.

One trap this query misses: views. Supabase's own docs say "Views bypass RLS by default because they are usually created with the postgres user." A view over a protected table can hand out every row. On Postgres 15 and above, create views like this so they follow the table's policies:

create view public.your_view
with (security_invoker = true)
as select ...;
Enter fullscreen mode Exit fullscreen mode

Check 2: in the repo (free, before you deploy)

The SQL check sees today's database. It doesn't see the migration your agent is about to push, or a key sitting in a client file.

I built a free command line check for this, VibeRaven. It runs on your machine and reads the repo: migrations, env files and client code. It never asks for your database credentials.

npx -y viberaven@1.6.1 check
Enter fullscreen mode Exit fullscreen mode

Here's what it prints on a small Next.js + Supabase test app with two common mistakes:

🔴 No RLS policy proof in migrations  (rls_disabled)
   1 public table without row level security: public.profiles
   (supabase/migrations/0001_init.sql:1). Anyone holding the anon key your
   frontend ships can read and change those rows through the Data API.
🔴 Service role key in a client-prefixed env variable  (service_role_key_in_client_env)
   .env.example:3: NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY names a service role
   or secret key under the NEXT_PUBLIC_ prefix. NEXT_PUBLIC_* variables are
   exposed to client code by Next.js, so anyone who loads the app can read it
   and skip RLS entirely. Rename it without the prefix, read it only in server
   code, and rotate the key.
🟡 A policy lets anyone read every row  (rls_policy_allows_all_read)
   Policy "anyone can read notes" on public.notes lets anyone, signed in or
   not, read every row: using (true). Fine for truly public content; wrong for
   anything per-user.
Enter fullscreen mode Exit fullscreen mode

Each finding names the file and line, so you can hand it straight to your agent.

What it can't do: it reads files, so a policy someone changed by hand in the dashboard is invisible to it. Use the SQL check above for that. It's advice for you to act on, not a security audit.

The short version

  1. Run the SQL query. Any table it lists with user data needs RLS today.
  2. Search your env files for NEXT_PUBLIC_ next to anything called service role or secret. If you find one, move it server side and rotate the key.
  3. Before your agent's next migration ships, check the repo too.

And one date to know: Supabase is changing how new tables reach the Data API. From October 30, new tables in existing projects stop being exposed automatically. That helps, but it doesn't fix the tables you already have.

UpGuard's full report: https://www.upguard.com/blog/everything-everywhere-systemic-data-exposure-in-supabase-apps

Top comments (0)