Skip to content
Blue Nebula

App rescue

Supabase RLS mistakes in AI-built apps, and how to fix them

The row-level security mistakes that leave Supabase data open in apps built with Lovable, Bolt or Replit, how to find each one, and the SQL to fix it.

By Luna R.Published 6 min read

The Supabase mistake I look for first in an AI-built app is a table anyone can read, because row-level security is off or a policy allows every row. Your publishable key ships to the browser, so RLS is what stands between a visitor and your data[1]. Open the Security Advisor, fix every error, then test with a second account.

When I rebuilt Social Snitch, an app first made with Replit’s AI agent, every table got row-level security from the start. These are the mistakes I check for when I take over an app built on Supabase, with the SQL to find and fix each one.

How RLS protects a Supabase app

Supabase serves your database through an API that the browser calls directly, using a publishable key (the older name is the anon key). Supabase calls that key safe to expose: anyone can read it, so it only reaches what row-level security allows[1].

RLS is a Postgres feature. Each policy is a condition on a table, and Postgres applies it to every row a query touches[2]. With RLS on and no policy for an action, the default is to deny, so nothing is returned and nothing changes[3].

That’s why a gap here matters. When a researcher scanned 1,645 apps from Lovable’s public showcase in 2025, 170 of them had inadequate RLS settings on at least one endpoint[4].

Mistake 1: RLS is off

If RLS isn’t enabled on a table in the public schema, Supabase warns that anyone with your project’s URL can create, read, update and delete its rows. The Security Advisor reports it as an error, rls_disabled_in_public[5].

Tables made in the dashboard’s Table Editor get RLS automatically. Tables created with SQL, which is how migrations create them, don’t[6]. Supabase is narrowing the gap: new projects since May 30, 2026 no longer expose new tables to the API automatically, and existing projects switch over on October 30, 2026[7]. Tables you already have keep their current access[7], and the change doesn’t turn RLS on, so check them.

Find the tables without RLS in the SQL editor:

sql
select tablename
from pg_tables
where schemaname = 'public'
  and not rowsecurity;

Turn it on for each one, then add policies, since a table with RLS on and no policies returns nothing[3]:

sql
alter table public.notes enable row level security;

Mistake 2: a policy that lets everyone in

Here RLS is on, so the table passes a quick look, but the policy says using (true) and lets every request read every row. Checking that a policy exists isn’t enough. According to the researcher behind the Lovable report, the platform’s first publish-time scan checked only that[4].

Find the policies whose condition is simply true:

sql
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
  and (qual = 'true' or with_check = 'true');

Replace each one with a policy tied to the signed-in user. Wrapping auth.uid() in a select lets Postgres call it once per query instead of once per row[2]:

sql
drop policy "Anyone can read notes" on public.notes;

create policy "Users read their own notes"
on public.notes for select
to authenticated
using ((select auth.uid()) = user_id);

A table that really is public, like published blog posts, can keep a true policy for select. Make sure that policy covers reading only.

Mistake 3: writes that don’t check the new row

Policies for insert and update take a with check condition, which decides what a new or changed row may look like. It’s what stops a user from saving a row under someone else’s user_id[2]. For updates, using picks the existing rows a user can change and with check validates the result. If you leave with check out, Postgres applies the using condition to the new row as well[2], but writing both makes the rule obvious to whoever reads it next.

sql
create policy "Users add their own notes"
on public.notes for insert
to authenticated
with check ((select auth.uid()) = user_id);

create policy "Users update their own notes"
on public.notes for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

An update also needs a matching select policy, or it can’t reach the rows it’s meant to change[2]. The query in mistake 2 lists any with check (true) policies too.

Mistake 4: trusting data the user can edit

A signed-in user can update their own user_metadata (stored as raw_user_meta_data), so Supabase says it isn’t a place for authorization data. app_metadata can’t be changed by the user, which makes it the right place for roles[2]. A policy that reads a role from user_metadata lets anyone give themselves that role, and the Security Advisor flags it as rls_references_user_metadata[8].

sql
-- Risky: users can edit user_metadata themselves
using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')

-- Safer: users can't change app_metadata
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')

A table of roles that users can read but not write works too.

Mistake 5: a view that skips RLS

Postgres views run with their owner’s permissions by default[9]. In Supabase that owner is usually the postgres user, so a view can return rows that RLS hides on the table underneath. Supabase calls this a security definer view, and the Security Advisor reports it as an error, security_definer_view[2][10]. On Postgres 15 and later, make the view respect the RLS of its tables[2]:

sql
alter view public.notes_summary set (security_invoker = true);

Mistake 6: the secret key in front-end code

The secret key, the newer name for the service_role key, bypasses every RLS policy, and Supabase says never to put one in a browser, a shipped app or source control[1]. Search your front-end code for sb_secret_ and service_role. If either turns up, rotate the key and move that code to a server function.

Supabase is retiring the legacy anon and service_role keys by the end of 2026[1]. Switching to the new keys is a good moment to check where each one is used.

Check your project in 15 minutes

  1. In the Supabase dashboard, open the Security Advisor and fix every error it lists[5].
  2. Run the two queries above in the SQL editor.
  3. Call the API with only the publishable key, the way an anonymous visitor would (the request is below). For any table that holds private data, you should get an empty list or an error.
  4. Sign in as two test users. Create a few rows as the first, then check that the second can’t see or change them.
  5. Run the Security Advisor again after every database change, since a table created with SQL starts without RLS[6].

The request for step 3:

Shell
curl "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

Sources

01

Supabase Documentation. Understanding API keys. Retrieved from https://supabase.com/docs/guides/api/api-keys

02

Supabase Documentation. Row Level Security. Retrieved from https://supabase.com/docs/guides/database/postgres/row-level-security

03

PostgreSQL Documentation. Row security policies. Retrieved from https://www.postgresql.org/docs/current/ddl-rowsecurity.html

04

Palmer, M. (2025, May 29). Statement on CVE-2025-48757 and the technical advisory CVE-2025-48757. Retrieved from https://mattpalmer.io/posts/statement-on-CVE-2025-48757/ and https://mattpalmer.io/posts/CVE-2025-48757/

05

Supabase Documentation. Database advisors: RLS disabled in public. Retrieved from https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public

06

Supabase Documentation. Tables and data. Retrieved from https://supabase.com/docs/guides/database/tables

07

Supabase. (2026, Apr 28). Breaking change: tables not exposed to Data and GraphQL API automatically (GitHub discussion #45329). Retrieved from https://github.com/orgs/supabase/discussions/45329

08

Supabase Documentation. Database advisors: RLS references user metadata. Retrieved from https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata

09

PostgreSQL Documentation. CREATE VIEW. Retrieved from https://www.postgresql.org/docs/current/sql-createview.html

10

Supabase Documentation. Database advisors: security definer view. Retrieved from https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view

Written by

Luna R.

Founder and engineer at Blue Nebula, a one-person studio in Daly City, CA, that builds web apps, Shopify stores and AI features. Top Rated Plus on Upwork with 3,700+ hours of client work.

More about me

Have a project in mind?

Tell me what you’re building.

mail@bluenebula.dev
Start a project