How to

How to Enable Supabase Row-Level Security in Your AI-Built App (Before You Go Live)

Rakesh Purohit

By Rakesh Purohit

Jul 31, 2026

Updated Jul 31, 2026

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."

RLS Disabled vs Enabled — database exposure comparison

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_id silently fails for unauthenticated users because null = user_id is 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 );
OperationClauseWhat It Controls
SELECTUSINGWhich existing rows the user can read
INSERTWITH CHECKWhether new row data meets policy conditions
UPDATEUSING + WITH CHECKWhich rows can be modified and what the new values must satisfy
DELETEUSINGWhich 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.

3 RLS Policy Patterns — single-tenant, multi-tenant, mixed visibility

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."

PatternPolicy LogicExample AppTypical Table Structure
Single-tenantauth.uid() = user_idPersonal CRM, habit trackeruser_id uuid references auth.users
Multi-tenantorg_id in (select org_id from members where user_id = auth.uid())Team project tool, B2B SaaSorg_id uuid, separate members table
Mixed visibilityOwner: auth.uid() = author_id / Public: is_public = trueBlog platform, marketplaceauthor_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.

RLS Generation at Build Time — 4-step process

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(), the user_id column 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_id calls the function on every row. Writing (select auth.uid()) = user_id caches 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.

RLS Performance Impact — data-driven benchmark comparison

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.

ApproachWhere It RunsWhat It ProtectsKey Limitation
RLS (Supabase)PostgreSQL engineEvery query, including direct DB accessRequires correct policy authorship
API-level authApplication serverAPI routes and business logicDoes not protect direct DB connections
Grant permissionsPostgreSQL rolesTable and column access by roleCoarse-grained, no per-row control
API gateway filteringNetwork layerRate limits, IP blocksNo data-level awareness

RLS vs Other Access Control Layers — four-layer security comparison

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.

About Author

Photo of Rakesh Purohit

Rakesh Purohit

DevRel Engineer

Product-led Growth, Technical Content on product's feature awareness through use cases, Community on Discord, Frontend architect for latency and performance with 6+ years of experience, Tinkerer, Thinker.

Decorative background for the call-to-action section

The work is only as good as the thinking before it.

You already know what you're trying to figure out. Type it. Rocket handles everything after that.