What is Supabase Row Level Security?
Row Level Security (RLS) is a PostgreSQL feature that Supabase exposes to all projects. When RLS is enabled on a table, every query against that table must satisfy a policy — a SQL condition that determines which rows a given user can read, insert, update, or delete. Without a policy in place, no rows are returned for that operation (a deny-by-default model).
Supabase exposes your database via a REST API at https://[project-ref].supabase.co/rest/v1/. Any client — including an unauthenticated browser request — can query this endpoint using your project's anon key, which is embedded in every Supabase client-side app by design. The anon key is not a secret; it is published in the browser bundle. What prevents data leakage is RLS — policies that tell the database to return only rows the requesting user is authorized to see.
If you call supabase.from('profiles').select('*') and RLS is disabled on the profiles table, an unauthenticated caller gets every row in the table. This is not a Supabase bug — it is the expected behavior of an unrestricted table. It is also OWASP API Security Top 10 A01: Broken Object Level Authorization (BOLA), and it is the root cause of CVE-2025-48757, which exposed user data from 170+ Lovable-built apps in May 2025.
The most common RLS misconfigurations in vibe-coded apps
AI coding tools often generate Supabase schemas with RLS disabled — they default to a simpler development configuration that lets any query succeed, which makes the scaffold easier to build on. Founders ship this configuration to production without realizing the implication. The three most common patterns found by Launch Ready Code across 700+ scanned apps are:
- RLS disabled entirely — the table was created without enabling RLS, making all rows readable via the anon key with no policy required
- RLS enabled with a permissive SELECT policy — RLS is on, but the SELECT policy is
using (true), which returns all rows to any authenticated or unauthenticated caller
- Missing write policies — SELECT is restricted by owner, but INSERT and UPDATE have no policy, letting any authenticated user create or modify rows they should not own
How to write a correct RLS policy
For a typical user-owned table (where each row belongs to one user), the minimum required policy restricts SELECT, INSERT, UPDATE, and DELETE to the row owner using auth.uid(). A correct set of policies for a documents table where each row has a user_id column looks like this:
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- SELECT: only the owner can read their rows
CREATE POLICY "select_own" ON documents
FOR SELECT USING (auth.uid() = user_id);
-- INSERT: only authenticated users can insert, and only as themselves
CREATE POLICY "insert_own" ON documents
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- UPDATE: only the owner can update their rows
CREATE POLICY "update_own" ON documents
FOR UPDATE USING (auth.uid() = user_id);
-- DELETE: only the owner can delete their rows
CREATE POLICY "delete_own" ON documents
FOR DELETE USING (auth.uid() = user_id);
Service-role keys bypass RLS by design — this is correct behavior for server-side operations that run with admin privileges. Never expose your service-role key in a client-side bundle. Only the anon key should be in your frontend code, and RLS is what makes the anon key safe to expose.
This tool makes read-only requests to the Supabase REST API from your browser. Your anon key is never sent to launchreadycode.com servers. launchreadycode.com