Row Level Security (RLS)
Row Level Security is a PostgreSQL feature that restricts which rows a user can access in a database table based on security policies defined at the database level.
Understanding Row Level Security (RLS)
Row Level Security moves access control from the application layer to the database itself. Instead of relying on application code to filter queries by user ID, RLS policies are enforced by PostgreSQL for every query, regardless of how the query originates. This means even if application-level checks are bypassed, the database still prevents unauthorized access.
In Supabase, RLS is the primary security mechanism. Since the Supabase anon key is public and included in frontend code, the database must enforce access control directly. When RLS is enabled on a table, no rows are accessible by default. You must create policies that define who can SELECT, INSERT, UPDATE, and DELETE rows. A typical policy checks that auth.uid() matches a user_id column on the row.
Common misconfigurations include forgetting to enable RLS on new tables (they default to open), creating overly permissive policies like USING (true), writing policies that check authentication for SELECT but not for UPDATE or DELETE, and using auth.uid() directly instead of (SELECT auth.uid()) which causes performance issues by re-evaluating for each row.
RLS policies support complex logic including joins to other tables, role-based access through JWT claims, and time-based restrictions. However, complex policies can impact query performance, so it is important to keep them as simple as possible and ensure relevant columns are indexed.
Why This Matters for Vibe-Coded Apps
RLS misconfiguration is the single most common critical vulnerability in vibe-coded Supabase applications. AI code generators routinely create tables without enabling RLS or add placeholder policies like USING (true) that grant access to everyone. When you prompt an AI to create a Supabase table, it often focuses on the schema and skips security entirely.
After any AI generates Supabase migration code, you must verify three things: RLS is enabled with ALTER TABLE ... ENABLE ROW LEVEL SECURITY, policies exist for all four operations (SELECT, INSERT, UPDATE, DELETE), and policies actually check the user identity with (SELECT auth.uid()). Missing any of these leaves your user data exposed to anyone with the public anon key.
Real-World Examples
Supabase Default Table Exposure
Numerous Supabase applications have been found with RLS disabled on user data tables. Since the anon key is public, anyone could query the Supabase REST API directly and extract all user records, messages, or payment information without authentication.
Partial Policy Coverage
A common pattern in production apps is having a SELECT policy that properly filters by user_id but missing DELETE or UPDATE policies. Attackers discover they can modify or remove other users' data even though they cannot read it, because the missing policies default to deny for the intended operation but the table lacks coverage for destructive operations.
Overly Broad Admin Policies
Developers sometimes create RLS policies granting full access to service_role or specific user IDs hardcoded in policies. If the service role key leaks or the admin user ID is guessed, the entire table is exposed. Proper admin access should use server-side service role calls that bypass RLS entirely, not permissive policies.
Frequently Asked Questions
What happens if I forget to enable RLS on a Supabase table?
The table is completely open. Anyone with your Supabase URL and the public anon key — both of which are visible in your frontend code — can read, insert, update, and delete every row in that table using the REST API directly. This is the most common vulnerability in Supabase applications and the reason Vibe App Scanner specifically checks for it.
Should I use RLS or application-level security?
Use both, but RLS should be your primary defense when using Supabase. Application-level checks can be bypassed by calling the API directly, while RLS is enforced by PostgreSQL itself and cannot be circumvented through the Supabase client. Think of RLS as your security floor — application logic can add restrictions on top, but RLS ensures the minimum level of protection is always present.
Why use (SELECT auth.uid()) instead of auth.uid() in policies?
Wrapping auth.uid() in a SELECT subquery tells PostgreSQL to evaluate it once per query instead of once per row. Without the subquery, PostgreSQL may re-evaluate auth.uid() for every row being checked, which degrades performance significantly on large tables. This optimization is recommended by Supabase and eliminates the auth_rls_initplan performance warning.
Can RLS policies be too complex?
Yes. RLS policies that join multiple tables or perform expensive computations are evaluated for every affected row, which can severely impact query performance. Keep policies simple — ideally a direct comparison of auth.uid() to a column. If you need complex authorization logic, consider denormalizing permission data into the table itself or using a materialized permissions table.
Is Your App Protected?
VAS automatically scans for vulnerabilities related to row level security (rls) and provides detailed remediation guidance. Our scanner targets issues common in AI-generated applications.
Scans from $5, results in minutes. Get actionable fixes tailored to your stack.
Get Starter Scan