BackShipLabs
Strategic Briefs
Application Security
October 23, 20269 min read

Supabase Row-Level Security: The Mistakes That Expose Every User's Data

Focus Area
Database Access Control
System Architecture
Supabase · Postgres RLS
Publisher
ShipLabs Engineering

Supabase lets your frontend query Postgres directly. That's a large part of why AI app builders like it: there's no backend to write. It also means your database is reachable from the public internet, and the key your app uses to reach it is visible to anyone who opens developer tools.

That's not a mistake on Supabase's part. The anon (or publishable) key is meant to be public. It identifies your project and tells the database the request comes from an anonymous user. What decides which rows that user can see is row-level security (RLS): Postgres policies attached to each table. When RLS is set up properly, a public key is harmless. When it isn't, the key opens everything.

How much this matters became clear in 2025, when a researcher scanned 1,645 apps built with Lovable and found about 170 whose Supabase tables could be read or written using only the public key. The issue became CVE-2025-48757. Lovable disputes the CVE, saying each customer is responsible for their own data. Whoever is right, the practical lesson is the same: check your own policies.

Check your project first

Start with the Security Advisor in the Supabase dashboard, which flags tables in exposed schemas that have RLS disabled. Then run this query in the SQL editor to see every table in the public schema and whether RLS is on:

Which public tables have RLS disabled?sql
select tablename, rowsecurity as rls_enabled
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Finally, test what an anonymous visitor actually gets. Use your project's URL and anon key, the same values your frontend uses, against a table that should be private. Only do this against a project you own.

Query your own project as an anonymous userbash
curl "https://<your-project>.supabase.co/rest/v1/profiles?select=*&limit=5" \
  -H "apikey: $SUPABASE_ANON_KEY" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY"

# Expected for a private table: []
# If you see other users' rows, RLS is off or a policy is too broad.

Mistake 1: RLS was never turned on

Supabase enables RLS automatically for tables created in the dashboard's Table Editor. Tables created with SQL, in the SQL editor or in migration files, start with RLS off. AI assistants usually generate SQL migrations, so their tables often start out unprotected.

Enable RLS on every table in an exposed schemasql
alter table public.profiles enable row level security;
alter table public.orders enable row level security;
-- repeat for every table in public

Once RLS is on and no policies exist, every request from the anon and authenticated roles returns nothing. That's the safe default. It also leads straight to the next mistake.

Mistake 2: A policy that allows everything

After RLS is enabled, the app suddenly shows empty lists. Ask an AI assistant to fix that and a common answer is a policy with using (true). The data comes back and the app works again, but the policy lets everyone read every row, which is where you started.

Too broad vs. scoped to the signed-in usersql
-- Often generated to make the error go away. Anyone can read every profile.
create policy "Enable read access for all users"
on public.profiles for select
using (true);

-- What you usually want: signed-in users read only their own row.
create policy "Users can read their own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = id);

using (true) is fine for data that really is public, such as a product catalog. For anything tied to a user or a team, the policy should compare a column against auth.uid() or a membership table. Write separate policies for select, insert, update, and delete, and only add the ones the app needs.

Mistake 3: Roles read from user-editable metadata

To add admin features, assistants sometimes store a role in the user's metadata and check it in a policy. The problem is that user_metadata can be changed by the user through the regular client library. Anyone can make themselves an admin with one call to supabase.auth.updateUser.

Don't authorize from user_metadatasql
-- Unsafe: user_metadata is writable by the user
using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')

-- Safer: app_metadata can only be set server-side
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')

app_metadata can only be changed with the service role key, so users can't edit it. For anything more complex than one admin flag, a roles or memberships table that only the server can write to is easier to reason about and audit.

Mistake 4: Views that quietly skip RLS

Views are a neat way to give the frontend a joined or summarized table. But in Postgres, a view runs with the privileges of the user who created it. In Supabase that's usually the postgres role, which bypasses RLS. A view over a protected table can expose every row of it.

Make the view respect the caller's policies (Postgres 15+)sql
create view public.order_summaries
with (security_invoker = true)
as
select id, user_id, total, created_at
from public.orders;

-- Or fix an existing view
alter view public.order_summaries set (security_invoker = true);

On older Postgres versions, revoke access to the view from the anon and authenticated roles, or move it to a schema that isn't exposed through the API. The same caution applies to database functions marked security definer in the public schema, because they can be called through the API and also run with elevated privileges.

Mistake 5: The service role key in the frontend

The service role (or secret) key bypasses RLS entirely. It belongs only on a server. If a policy is blocking something and the assistant switches the frontend to the service role key, every policy you wrote stops applying for every visitor.

Search your code and environment files for the service role key in any variable starting with NEXT_PUBLIC_ or VITE_. If it was ever exposed in a deployed bundle or a public repository, rotate it in the dashboard, then move the code that needed it into a server route.

Keep it fixed

RLS problems tend to come back, because every new table needs its own policies and every AI-generated migration is a chance to forget. Some habits that help:

  • Keep RLS and policies in migration files under version control, so they're reviewed like code.
  • Add a CI step that runs the pg_tables query above and fails if any public table has RLS disabled.
  • Write policy tests with two test users using supabase test db (pgTAP), checking that user B can't read, update, or delete user A's rows.
  • Re-run the Security Advisor after every schema change, not just before launch.

Where we stand: this guide is based on Supabase's public documentation and standard Postgres behavior as of October 2026. Details of CVE-2025-48757 come from the public CVE record and published research. Test only projects you own or have written permission to assess.

Checks Covered

"RLS enabled on every public table · Policies scoped to auth.uid() · Roles from app_metadata, not user_metadata · Views set to security_invoker · Service role key kept server-side"

Related Engineering Service

Security Assessments & Production Hardening

We review Supabase and Firebase access rules, write and test the policies your app needs, and add CI checks so new tables don't ship unprotected.

Explore Technical Architecture
Related Strategic Briefs
Application Security
Five Security Holes Common in Vibe-Coded Apps (and How to Check for Each)

AI coding tools are very good at getting an app to work. They are much less reliable at stopping it from doing things it shouldn't. Here are the five gaps that show up most often, and a check for each one you can run yourself before launch.

Read Brief
AI Architecture
Decision Models for the Enterprise: Jev, Clef, and OpenAI's Decisions API

Jev, Clef, and OpenAI's Decisions API shipped within three weeks of each other, and none of them write text. Here's what the requests actually look like, how the three differ, and the adapter and cascade we'd put around them in production.

Read Brief
Finance Operations
Invoice Reconciliation & Payment Controls at Scale

Enterprise finance teams spend thousands of manual hours reconciling supplier invoices against purchase orders, goods receipts, and payment ledgers. Here is how we automated it.

Read Brief
Commerce Operations
Multilingual Voice Orders & Demand Forecasting

High-volume restaurants and service networks lose order potential during peak traffic because their teams cannot answer every call. We built low-latency multilingual voice systems linked directly to demand and procurement forecasts.

Read Brief
Risk & Governance
Regulatory Change Management with Deterministic RAG

Regulatory requirements can change overnight. We built deterministic Graph RAG architectures to monitor authoritative updates and automatically suggest redlines to existing policies and agreements.

Read Brief
Direct Consult

Have similar structural bottlenecks?

We deploy highly deterministic agent systems directly into enterprise database and pipeline topologies. Drop us your specifications or book a technical strategy sync.

Technical Strategy Sync

Book a direct 30-minute sync to dissect your pipeline schemas:

Book 30-Min Call
Drop a Brief

Send details of your architecture directly to our developer team:

hello@shiplabs.app