RLS in Supabase means Row Level Security. It is the protection layer that stops User B from reading or changing User A's private rows through the Supabase API. If your AI-built app stores customer data, messages, documents, invoices, profiles, or team records in Supabase, RLS is usually the difference between a safe launch and a cross-user data leak.
Supabase RLS is one of the most important checks before launching a Lovable, Cursor, Bolt, Replit, or v0 app. Your app can look finished, login can work, and the dashboard can show the right data, while the database is still open to the wrong user.
The reason is simple: AI app builders often generate the happy path first. They create tables, wire the frontend to Supabase, and make the demo work. But production safety depends on whether each row is protected by a correct policy.
Fast check: if your app stores user data in Supabase and any table in the public schema has RLS disabled, do not launch until you understand why.
What is RLS in Supabase?
RLS is a Postgres security feature used by Supabase to control access at the row level. Instead of only asking "is this user logged in?", RLS asks a more important question: "is this specific user allowed to access this specific row?"
Without RLS, a browser app can accidentally expose data through direct API calls even when the UI looks correct. With RLS enabled and correct policies in place, Supabase blocks rows by default unless a policy explicitly allows the current user to access them.
Why RLS matters in AI-built apps
Supabase is designed so frontend apps can use a public key. That public key is not the problem by itself. The problem is relying on that key while missing the policies that decide which rows the current user may read, insert, update, or delete.
The official Supabase RLS docs state that RLS should be enabled on tables in exposed schemas, including the default public schema. They also explain the secure default after RLS is enabled: no data is accessible through the API until policies allow it.
Supabase RLS docs are not enough before launch
Supabase's row level security docs explain the mechanism: enable RLS, write policies, and keep privileged keys server-side. That is necessary, but it is not the same as proving your SaaS is safe for real users.
Before launch, test real product scenarios. Create User A and User B, add private records for both, and verify that neither user can read, update, delete, list, download, or overwrite the other's data. Repeat the same check for storage buckets, team or organization membership, admin-only flows, and any route that uses the service_role key on the backend.
The docs tell you how RLS should work. A launch check proves your actual Lovable, Cursor, Bolt, Replit, or v0 app enforces those rules after the AI-generated UI, API calls, storage paths, and database policies are connected.
Recent Reddit discussions around Supabase and vibe-coded apps show the same pattern from the field: RLS trips up builders, AI tools can silently break policy logic, and service role keys sometimes leak into client code. GitHub projects have appeared specifically to scan Lovable, Cursor, Bolt, and v0 apps for missing RLS, permissive policies, exposed service_role keys, and unsafe auth logic.
1. List every public table and check RLS status
Start with the database, not the UI. The UI only proves what your current account can see. It does not prove another user is blocked.
Run this in Supabase SQL Editor to list tables in the public schema and whether RLS is enabled:
select
c.relname as table_name,
c.relrowsecurity as rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r'
and n.nspname = 'public'
order by c.relname;
Any table that stores users, profiles, accounts, teams, subscriptions, documents, messages, orders, invoices, files, leads, or private settings should have RLS enabled unless it is intentionally public.
2. Check that policies exist for the operations your app uses
RLS enabled with no policy is usually safer than RLS disabled: it blocks access by default. But if your app needs reads and writes, you need policies that match the actual operations.
List policies with:
select
schemaname,
tablename,
policyname,
cmd,
roles,
qual,
with_check
from pg_policies
where schemaname = 'public'
order by tablename, policyname;
What to look for
- Private tables should not use
USING (true)unless the data is intentionally public. SELECTpolicies should scope rows to the current user, team, or tenant.INSERTpolicies should useWITH CHECKso users cannot insert rows owned by someone else.UPDATEpolicies should usually have bothUSINGandWITH CHECK.DELETEpolicies should be explicit, not accidentally inherited from a broadFOR ALLpolicy.
3. Test with User A and User B
Do not trust a policy just because it compiles. RLS bugs are often silent. The minimum real test is two-user isolation.
- Create User A and User B.
- As User A, create records in every private table.
- As User B, try to read User A's rows through the UI.
- As User B, try direct API calls using User A's row IDs.
- Try update and delete paths, not only reads.
If User B can read or mutate User A's private data, the app is not ready for production.
4. Verify ownership logic
The common policy pattern is not "user is logged in." It is "this row belongs to this user or to a tenant the user is allowed to access."
For a simple user-owned table, a policy often looks like this:
create policy "Users can read their own documents"
on documents
for select
to authenticated
using (user_id = auth.uid());
For inserts, the important part is WITH CHECK:
create policy "Users can create their own documents"
on documents
for insert
to authenticated
with check (user_id = auth.uid());
Without WITH CHECK, users may be able to write rows with another user's ID or move a record into a tenant they do not own.
5. Keep the service role key out of the browser
The Supabase service role key bypasses RLS. It is useful for trusted backend jobs, but it should never be in frontend code, browser bundles, public repos, or variables prefixed with NEXT_PUBLIC_, VITE_, or REACT_APP_.
Search for it
- Search your repo for
service_role. - Search built JavaScript files for Supabase JWT-looking keys.
- Search GitHub history, not only the current commit.
- Search Lovable or Cursor prompts where secrets may have been pasted during debugging.
If the service role key was exposed, rotate it in Supabase and remove every copy. Do not rely on deleting the current line from code.
6. Check storage buckets, not only tables
Many apps store invoices, avatars, exports, generated images, or user uploads in Supabase Storage. Table RLS does not automatically make every file private. Storage has its own access model and policies.
What to check
- Private user files are in private buckets.
- Users cannot list another user's files.
- Downloads use signed URLs with reasonable expiry when files are private.
- Upload paths cannot be overwritten by another user.
- Policies check owner or tenant, not just authenticated status.
7. Watch for views, RPC functions, and Edge Functions
RLS checks can be bypassed or weakened through surrounding database features if they are not designed carefully. Supabase's RLS docs call out that views can bypass RLS by default depending on how they are created. RPC functions and Edge Functions can also accidentally expose data if they use broad service role access or skip ownership checks.
What to check
- Views that expose private data are reviewed for RLS behavior.
- RPC functions validate the current user and tenant.
- Edge Functions that use service role perform their own authorization checks.
- Admin-only functions are not callable by normal authenticated users.
- Realtime subscriptions do not leak rows from private tables.
8. Use Supabase Security Advisor and a code scan
Supabase includes security tooling that can flag important configuration problems. Run it before launch, but do not stop there. Static scanners and GitHub projects focused on AI-built apps now check patterns like missing ENABLE ROW LEVEL SECURITY, USING (true), service role keys in client files, committed .env files, and unsafe route handlers.
Scanners are useful because they catch mechanical issues quickly. They do not replace the User A/User B test, because they do not always know your actual tenant model or business rules.
Supabase RLS launch checklist
- Every private public-schema table has RLS enabled.
- Every private table has policies for the operations your app uses.
- No private policy uses broad logic like
USING (true). INSERTandUPDATEpolicies useWITH CHECKwhere ownership can change.- User A cannot read, update, or delete User B's rows.
- The service role key exists only in trusted server-side code.
- Storage buckets and file paths are tested for cross-user access.
- Views, RPC functions, Edge Functions, and Realtime are reviewed.
- Supabase Security Advisor and a repo scan are clean or understood.
- RLS tests are repeated after every database schema change.
When to ask for help
Ask for a Supabase RLS audit if your app has real users, teams, paid accounts, private files, customer data, or anything that would be painful to leak. This is especially important for AI-built apps where policies may have been generated quickly and never tested against another user.
Sources used
- Supabase docs: Row Level Security
- Supabase docs: API keys
- Supabase docs: Storage access control
- PostgreSQL docs: Row Security Policies
- Reddit: Supabase RLS policies trip up builders
- Reddit: Cursor and Claude breaking RLS and service role safety
- GitHub: vibe-code-triage scanner for AI-generated apps
- GitHub: vibe-scanner for Supabase and Lovable RLS policies
Want a senior developer to check your Supabase RLS before launch?
I review RLS policies, service role exposure, storage access, auth ownership checks, and cross-user data isolation in AI-built SaaS apps.
Get a Launch Audit See sample report