RLS is a PostgreSQL feature that restricts database access per user. Without it, any authenticated user can read every row in your app. Rocket.new generates correct policies from a single Build prompt, so your data stays protected from day one.
Supabase row-level security (RLS) is the database-level access control layer that separates a working demo from a production-safe app. Without it, every authenticated user can read every row in your database. This guide covers enabling RLS, writing correct policies for three common app patterns, and generating them automatically from a single prompt.
What you'll learn in this guide:
- Why RLS is off by default for SQL-created tables and what that exposes
- How PostgreSQL evaluates policies on every query (USING vs. WITH CHECK)
- Exact SQL for single-tenant, multi-tenant, and mixed-visibility patterns
- How to test that policies actually block unauthorized access
- The four performance mistakes that slow RLS queries and how to fix them
What Happens When RLS Stays Disabled?
The default state of every Supabase table created via raw SQL or the SQL editor is RLS off. That single default turns your database into a public access point where every logged-in user gets the same data.
- When you create table public.todos without enabling row level security, any authenticated user can run select * from todos and receive every row. Not just their own todos, but everyone's.
- The Supabase client connects using the anon key by default. Without RLS policies in place, the anon key grants read access to every row in every table in the public schema.
- Consider a simple notes app. User A signs in and calls
.from('notes').select(). Without policies, they receive User B's private notes too, because the query returns everything. No access control exists at the database level. - Tables created through the Supabase dashboard table editor have RLS enabled automatically. But tables created through migrations, raw SQL, or AI-generated code almost never do. As the Supabase RLS documentation states: "If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself."

With RLS disabled, every authenticated user reaches every row. With RLS enabled, each user sees only what their policy permits.
The safety net does not exist until you build it. Most AI code generators never prompt you to add it, which is why vibe-coded apps frequently ship with critical security gaps.
How Does the Policy Engine Actually Work?
RLS policies are PostgreSQL's native rule engine for row-level access control. Each policy attaches to a specific table and runs on every query against that table, acting like an invisible WHERE clause.
A policy defines who can do what on which rows. You create policy statements specifying the operation (SELECT, INSERT, UPDATE, DELETE), the target role (authenticated, anon), and a boolean expression that must return true for the row to be accessible. Per the Supabase RLS docs: "You can just think of them as adding a WHERE clause to every query."
- The USING clause determines which existing rows a user can see or modify. The WITH CHECK clause validates new row data during INSERT or UPDATE operations.
- When Supabase receives an authenticated request, it checks the JWT, maps the request to a Postgres role, and evaluates every active policy on the target table. If no policy returns true, the row stays inaccessible.
- Policies are permissive by default. If any single permissive policy passes, access is granted. Restrictive policies work differently: all restrictive policies must pass in addition to at least one permissive policy.
- Important: auth.uid() returns null for unauthenticated requests. A policy using
auth.uid() = user_idsilently fails for unauthenticated users becausenull = user_idis always false in SQL. For clarity, explicitly check:USING (auth.uid() IS NOT NULL AND auth.uid() = user_id).
Supabase evaluates RLS policies on every query before returning any data
So when a user queries your todos table, the database itself decides which rows that specific user ID is allowed to see. The application code never needs to filter manually.
Which SQL Statements Do You Need for Each Operation?
Each database operation requires its own policy. Here is the complete pattern for a table where users manage their own todos, grounded in the official Supabase RLS documentation.
First, enable RLS and set up the table:
1-- Create the table
2create table public.todos (
3 id uuid primary key default gen_random_uuid(),
4 user_id uuid references auth.users not null,
5 title text,
6 completed boolean default false
7);
8
9-- Enable row level security
10alter table public.todos enable row level security;
11
12-- Grant permissions to roles
13grant select, insert, update, delete on public.todos to authenticated;
Now create policy statements for each operation:
1-- SELECT: Users can only see their own todos
2create policy "Users can view own todos"
3 on public.todos for select
4 to authenticated
5 using ( (select auth.uid()) = user_id );
6
7-- INSERT: Users can only insert rows with their own user_id
8create policy "Users can create own todos"
9 on public.todos for insert
10 to authenticated
11 with check ( (select auth.uid()) = user_id );
12
13-- UPDATE: Users can only update their own todos
14create policy "Users can update own todos"
15 on public.todos for update
16 to authenticated
17 using ( (select auth.uid()) = user_id )
18 with check ( (select auth.uid()) = user_id );
19
20-- DELETE: Users can only delete their own todos
21create policy "Users can delete own todos"
22 on public.todos for delete
23 to authenticated
24 using ( (select auth.uid()) = user_id );
| Operation | Clause | What It Controls |
|---|---|---|
| SELECT | USING | Which existing rows the user can read |
| INSERT | WITH CHECK | Whether new row data meets policy conditions |
| UPDATE | USING + WITH CHECK | Which rows can be modified and what the new values must satisfy |
| DELETE | USING | Which existing rows the user can remove |
Notice that every policy wraps auth.uid() inside a select statement. This is a performance pattern documented by Supabase that caches the function result per query instead of calling it on each row. Without the select wrapper, RLS policy performance degrades dramatically on large tables.
What Are the Three Most Common App Patterns That Need RLS?
Not every app follows the same access control model. The three patterns below cover the majority of vibe-coded applications, and each requires a different approach to policies.
- Pattern 1: Single-tenant SaaS (user-owned data). Each user owns their data exclusively. The policy is simple:
(select auth.uid()) = user_id. This covers personal dashboards, note apps, and single-player tools where nobody else should access your rows. - Pattern 2: Multi-tenant B2B. Team members share access to rows based on an
organization_id.Multi-tenant applications need policies that reference a join table or app_metadata in the JWT. - Pattern 3: Mixed visibility (public + private). Creators have full CRUD on their own posts while other users get read-only access to items marked public. This combines two permissive policies: one for owners and one for public posts where
is_public = true.

The three RLS patterns that cover the majority of app architectures
As GaryAustin1 noted in the Supabase RLS Performance discussion: "The impact of RLS on performance of your queries can be massive. This is especially true on queries that look at every row in a table."
| Pattern | Policy Logic | Example App | Typical Table Structure |
|---|---|---|---|
| Single-tenant | auth.uid() = user_id | Personal CRM, habit tracker | user_id uuid references auth.users |
| Multi-tenant | org_id in (select org_id from members where user_id = auth.uid()) | Team project tool, B2B SaaS | org_id uuid, separate members table |
| Mixed visibility | Owner: auth.uid() = author_id / Public: is_public = true | Blog platform, marketplace | author_id uuid, is_public boolean |
For multi-tenant patterns, the key decision is where to store membership data. JWT claims (via auth.jwt() -> 'app_metadata' -> 'org_id') give faster policy evaluation because no join query runs. A membership table is more flexible but needs a security definer function to bypass RLS on the lookup.
When building a B2B SaaS product with AI, getting these multi-tenant patterns right from the start prevents painful migrations later. You can also explore how to secure an app in production for a broader checklist of security layers beyond RLS.
How Do You Test That Policies Actually Block Unauthorized Access?
Testing policies before going live is as important as writing them. Here is what to verify:
- Open the SQL editor in the Supabase dashboard and switch to the authenticated role with a test JWT. Run set session role authenticated; followed by your query to see exactly what the policy returns.
- Use EXPLAIN ANALYZE to measure RLS policy performance. If a SELECT on a 100K-row table takes more than a few milliseconds, your policy likely needs an index on the column used in the USING clause.
- Test with multiple user IDs. Impersonate users by setting different JWT claims and confirm that User A cannot see User B's rows. A policy that silently returns zero rows can mask bugs where the USING clause never evaluates to true.
- You can bypass row level security for administrative tasks using the service_role key. This gives system-level access for migrations, data fixes, or admin panels, but should never run in client-side code.
Test queries should cover all four operations. Insert a row as User A, then attempt to SELECT, UPDATE, and DELETE it as User B. If any operation succeeds, your policies have a gap.
Why Rocket.new Generates RLS Policies as Part of the Build
Most AI builders treat the database as an afterthought. Rocket.new takes a different approach: when you connect Supabase, RLS policies become part of the generation itself, not a separate task you add later.
As Rocket.new's own engineering documentation explains: "When RLS is part of the generation, the migration that creates a table also creates its security policies. They're generated together because they belong together." The generation context already includes the database schema, authentication configuration, and Row-Level Security policies before a single line of app code is written.
- RLS policies ship with the table, not after it. Describe your app in a Build prompt and Rocket.new generates the schema alongside the matching security policies. The application code ships with enable RLS already applied to every table.
- Many AI builders default to RLS-off schemas unless explicitly prompted, producing working demos that pass every functional test but fail the first real-world security audit.
- The correct policy pattern is matched to your data model. Rocket.new analyzes foreign key relationships, identifies ownership columns, and writes the matching create policy statements, including the performance-friendly (
select auth.uid()) wrapper. - You can review and modify everything. The generated policies appear in your project's migration files. You own the SQL, you can edit it in any SQL editor, and you can add restrictive policies or security definer functions as your requirements grow.

Rocket.new generates RLS policies alongside table migrations in a single Build step
Build a project management app where:
- Users belong to organizations
- Each org has multiple projects
- Only org members can view, create, or edit projects within their org
- Org admins can delete projects
- Use Supabase for auth and database with proper RLS policies
Rocket.new translates this into create table public statements with org_id columns, a members lookup table, security definer functions for the join bypass, and enable row level security on every new table. You can also install the auto-enable RLS event trigger so future tables added during iteration are never left unprotected. If you want to see Rocket.new in action before building, you can explore live demos to understand how the generation pipeline works end to end.
For teams building production-ready mobile apps, having RLS generated at build time removes an entire class of security bugs before the first user ever signs up.
What Performance Mistakes Slow Down RLS Queries?
RLS adds a performance cost. Every SELECT operation evaluates your policy expression against potentially every row. Supabase's own benchmarks (published in their RLS performance documentation) show the following:
- Missing indexes on policy columns. If your policy checks
user_id = auth.uid(), theuser_idcolumn needs a btree index. Without it, PostgreSQL runs a sequential scan on every query. Supabase's benchmark shows adding the index improved performance from 171ms to under 0.1ms, a 99.94% improvement on a 100K-row test table. - Not wrapping functions in select. Writing
auth.uid() = user_idcalls the function on every row. Writing (select auth.uid()) = user_idcaches the result once per query. Supabase's benchmark shows this change reduced query time from 179ms to 9ms. - Complex join policies. When your policy joins back to the source table on every row, performance collapses. Supabase benchmarks show a slow join policy running at 9,000ms vs. 20ms after rewriting to avoid the join, a 99.78% improvement. Wrap the lookup in a security definer function that bypasses RLS on the join table for best results.
- Forgetting to specify the role. A policy without a TO authenticated clause evaluates for anon users too, wasting database cycles. Supabase benchmarks show specifying the role reduces execution from 170ms to under 0.1ms. Always specify the target role.

Query time improvements from three RLS optimizations, based on Supabase benchmark data
According to the 2026 IBM Cost of a Data Breach Report, the average breach now costs $4.99M globally, a 12% year-over-year increase. According to the 2026 Verizon DBIR, 31% of breaches now start with software vulnerabilities, not stolen credentials. In vibe-coded apps using Supabase, the most common vulnerability is simpler than a misconfigured API: row level security left disabled on every table.
For a complete web application security checklist, RLS should be the first item, not the last.
RLS vs. Other Access Control Approaches
A common question after learning RLS: "Is this enough, or do I still need API-level auth checks?" The answer is that RLS and application-level auth serve different layers and work best together.
| Approach | Where It Runs | What It Protects | Key Limitation |
|---|---|---|---|
| RLS (Supabase) | PostgreSQL engine | Every query, including direct DB access | Requires correct policy authorship |
| API-level auth | Application server | API routes and business logic | Does not protect direct DB connections |
| Grant permissions | PostgreSQL roles | Table and column access by role | Coarse-grained, no per-row control |
| API gateway filtering | Network layer | Rate limits, IP blocks | No data-level awareness |

RLS is the only layer that protects data at the database level, including direct connections and Realtime
RLS is the only layer that protects data even when accessed through third-party tooling, direct Postgres connections, or Supabase's own Realtime subscriptions. Use it as your foundation, then add API-level checks for business logic that RLS cannot express. For a deeper look at how AI app builders handle authentication and security by default, the architecture decisions made at generation time matter more than any post-launch patch.
Your First Security Layer Ships Before Your First Customer
Row level security is not an advanced topic reserved for senior engineers. It is the baseline permission logic that separates a demo from a product, and the difference between a soft launch and a data leak.
The pattern is straightforward: enable RLS, write one policy per operation per table, wrap your auth functions in select, and add indexes on your policy columns. Or let Rocket.new handle the entire flow from a single Build prompt so your app ships protected from the start.
Start building on Rocket.new and ship your app with production-ready Supabase row-level security policies from the first prompt. No manual SQL, no forgetting to enable RLS, no data exposure surprises.
Table of contents
- -What Happens When RLS Stays Disabled?
- -How Does the Policy Engine Actually Work?
- -Which SQL Statements Do You Need for Each Operation?
- -What Are the Three Most Common App Patterns That Need RLS?
- -How Do You Test That Policies Actually Block Unauthorized Access?
- -Why Rocket.new Generates RLS Policies as Part of the Build
- -What Performance Mistakes Slow Down RLS Queries?
- -RLS vs. Other Access Control Approaches
- -Your First Security Layer Ships Before Your First Customer




