Skip to main content

Supabase row-level security

In a typical Lovable app, the browser talks straight to Supabase using a public key. That is by design: the key is meant to be public, and every visitor's browser has it. Row-level security (RLS) is what stops one visitor from reading or rewriting another's data. If it is off, or a policy simply says true, the public key is all anyone needs.

How offramp reads your rules​

offramp replays every file in supabase/migrations/ in order, the way the database ran them, and checks the final state of each table in the public schema. A table whose RLS is enabled three migrations later is reported as clean, and a dropped table is not reported.

These three checks report could not assess, and give no answer, when:

  • the app uses Supabase but the repository has no migrations in supabase/migrations/, so its tables and policies were created in the dashboard;
  • migrations are not all in Supabase's <timestamp>_name.sql format, so the order they ran in is unknown;
  • a migration could not be replayed, for example because it uses dynamic SQL, so any answer would be partial.

They are not applicable when the app does not use Supabase.

supabase.rls.disabled​

Critical. A table in public where row-level security is never turned on. Anyone with the public key can read and change every row.

Fix: enable row-level security on the table and add policies scoped to the row's owner. Until then, treat the data in it as exposed.

alter table public.orders enable row level security;

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

supabase.rls.permissive_write​

High. An update, delete or all-operations policy that applies to anyone (anon or public) or to any signed-in user (authenticated), and whose condition is true. Anyone who can sign up can then rewrite or delete other people's rows.

Medium when the policy is an insert with true. That is usually a contact or waitlist form: worth checking for spam and oversized rows, but not the same risk as rewriting data.

Fix: replace true with a condition on the row's owner, for example auth.uid() = user_id, or remove the policy if nobody should do this.

supabase.rls.public_read​

Medium. A select policy that lets anyone read the whole table with a condition of true. Sometimes that is intended, as with a product catalogue, and sometimes it is not.

Fix: confirm the table holds nothing private. If it does, restrict the policy, or move the private columns to a table with its own policies.

What these checks cannot see​

Policies and RLS settings changed in the Supabase dashboard never reach supabase/migrations/, so they are invisible here. Run the Security Advisor in the Supabase dashboard as well. If anyone can sign up to your app, every authenticated policy is effectively public, so check your auth settings too. See what offramp cannot see.