• Agents
  • Docs
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Blog
  • Docs
  • FAQ
  • Glossary

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog
Security

Supabase RLS: The Security Feature Your AI Forgot to Enable

Row Level Security is the difference between a secure Supabase app and a public database. Here's how RLS works, why AI tools skip it, and how to set it up in 15 minutes.

Flowpatrol TeamMar 27, 202610 min read
Supabase RLS: The Security Feature Your AI Forgot to Enable

What is Supabase Row Level Security and why does it matter?

Row Level Security (RLS) is a PostgreSQL feature that controls which rows each user can read, create, update, or delete — enforced at the database level. Without it, your Supabase anon key (which is public, sitting in your JavaScript bundle) gives anyone full read/write access to every table. 68% of vibe-coded apps using Supabase or Firebase have RLS disabled or misconfigured. It's the #1 vulnerability in apps built with AI coding tools.

If you built an app with Lovable, Cursor, or Bolt that uses Supabase, there's a good chance anyone can read every row in your database right now. Not through some exotic hack — by copying two strings from your page source and running a single query. Your app works. Users can log in, see their dashboards, manage their content. Everything feels solid. But the database door is wide open.

The fix takes 15 minutes. Here's how RLS works, why AI tools skip it, and how to set it up.


How does RLS actually work?

Row Level Security is a PostgreSQL feature that Supabase exposes as a first-class concept. It lets you define policies that control which rows a user can see, create, update, or delete — enforced at the database level.

Without RLS, Supabase's access control works like this:

  • Anon key (public, in your JavaScript): full access to all rows in all tables
  • Service role key (secret, server-side only): full access, bypasses everything

With RLS enabled, it changes to:

  • Anon key: access determined by your policies — typically "users see only their own data"
  • Service role key: still full access (for admin operations)

The important shift is that the anon key goes from "master key" to "scoped key." That's what makes it safe to expose in client-side code.

Database table with three rows, each accessible only by the owning user — arrows connect each user to their own row


Why do AI tools skip RLS?

We've analyzed code generated by Lovable, Bolt, Cursor, and Claude for Supabase projects. The pattern is consistent: AI generates working database schemas but rarely enables RLS or creates policies.

Why?

1. AI optimizes for functionality, not security. When you say "build me a task manager," the AI focuses on making tasks work — creating, reading, updating, deleting. Access control isn't part of the functional requirement, so it doesn't get generated.

2. RLS requires understanding the data model. Writing good policies means understanding relationships: which user owns which data, what's public vs. private, who can share what with whom. AI can infer some of this, but it often doesn't.

3. Without RLS, everything still works. This is the trap. Your app functions perfectly without RLS. Users log in, see their data, everything looks right. The problem is invisible until someone exploits it.

4. Supabase's defaults don't help. When you create a new table in Supabase, RLS is disabled by default. There's no warning when you deploy. The happy path doesn't include security.

This combination — AI doesn't generate it, defaults don't require it, and apps work without it — is why RLS misconfiguration is the #1 vulnerability in vibe-coded applications.


How do you check if your database is exposed?

Run this query in your Supabase SQL Editor right now:

SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

If any row shows rowsecurity | false, that table is accessible to anyone with your anon key. Fix it before you do anything else.

To see what policies exist on tables that do have RLS enabled:

SELECT tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';

If a table has RLS enabled but no policies, it defaults to denying all access (which is safe but breaks your app). You need both: RLS enabled AND appropriate policies.


Setting up RLS from scratch

Let's walk through securing a typical app — say a simple task manager with users and tasks.

Step 1: Enable RLS on every table

ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE categories ENABLE ROW LEVEL SECURITY;

Do this for every table in your public schema. No exceptions.

Step 2: Create policies for user-owned data

The most common pattern: users can only access their own rows.

-- Users can read their own profile
CREATE POLICY "Users can view own profile"
ON users FOR SELECT
USING (auth.uid() = id);

-- Users can update their own profile
CREATE POLICY "Users can update own profile"
ON users FOR UPDATE
USING (auth.uid() = id);

-- Users can read their own tasks
CREATE POLICY "Users can view own tasks"
ON tasks FOR SELECT
USING (auth.uid() = user_id);

-- Users can create tasks (assigned to themselves)
CREATE POLICY "Users can create own tasks"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can update their own tasks
CREATE POLICY "Users can update own tasks"
ON tasks FOR UPDATE
USING (auth.uid() = user_id);

-- Users can delete their own tasks
CREATE POLICY "Users can delete own tasks"
ON tasks FOR DELETE
USING (auth.uid() = user_id);

Step 3: Handle shared data

Some data should be readable by multiple users. For example, categories might be shared across an organization:

-- Categories are readable by all authenticated users
CREATE POLICY "Authenticated users can view categories"
ON categories FOR SELECT
TO authenticated
USING (true);

-- Only admins can create categories
CREATE POLICY "Admins can manage categories"
ON categories FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM users
    WHERE users.id = auth.uid()
    AND users.role = 'admin'
  )
);

Step 4: Handle public data

If some data should be visible without authentication (like a public profile or published content):

-- Published posts are readable by anyone
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (published = true);

-- Authors can view all their own posts (including drafts)
CREATE POLICY "Authors can view own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);

Step 5: Verify it works

Test from an unauthenticated context:

# This should return empty or error — not your data
curl "https://YOUR_PROJECT.supabase.co/rest/v1/tasks?select=*" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

Then test as an authenticated user to confirm they see only their own rows:

const { data } = await supabase.from("tasks").select("*");
// Should return only the current user's tasks, not everyone's

Common RLS patterns

Here are the patterns you'll use most often:

Owner-based access

-- Simple: user owns the row
USING (auth.uid() = user_id)

-- With a foreign key: user owns the parent
USING (
  auth.uid() = (
    SELECT user_id FROM projects
    WHERE projects.id = project_id
  )
)

Role-based access

-- Check user's role from a profiles table
USING (
  EXISTS (
    SELECT 1 FROM profiles
    WHERE profiles.id = auth.uid()
    AND profiles.role IN ('admin', 'editor')
  )
)

Organization / team access

-- User belongs to the same org as the resource
USING (
  org_id IN (
    SELECT org_id FROM org_members
    WHERE user_id = auth.uid()
  )
)

Time-based access

-- Content is accessible after publish date
USING (published_at <= now())

The mistakes people make

Even when RLS is enabled, there are common mistakes:

Three common RLS mistakes illustrated — an overly permissive policy shown as a wide-open gate, a missing policy shown as a gap in a wall, and a service key exposed in client code shown as a key with an eye

Mistake 1: Using the service role key in client code

The service role key bypasses all RLS policies. If it's in your JavaScript bundle, RLS is effectively disabled.

// NEVER do this in client code
const supabase = createClient(url, SERVICE_ROLE_KEY);
// This bypasses RLS completely

Always use the anon key on the client. Use the service role key only in server-side code (API routes, server actions, edge functions).

Mistake 2: Overly permissive policies

-- This "enables" RLS but allows everything — useless
CREATE POLICY "allow all" ON tasks
FOR ALL USING (true);

Every policy should reference auth.uid() or restrict access in some meaningful way.

Mistake 3: Missing INSERT policies with WITH CHECK

USING controls which rows you can read/update/delete. WITH CHECK controls what you can insert. Without it, users might be able to create rows they shouldn't.

-- Good: ensures users can only create tasks for themselves
CREATE POLICY "Users create own tasks"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Bad: allows inserting tasks assigned to anyone
CREATE POLICY "Users create tasks"
ON tasks FOR INSERT
WITH CHECK (true);

Mistake 4: Forgetting about JOINs

If Table A has RLS and Table B doesn't, a query that joins them might leak data from Table B. Enable RLS on ALL tables, not just the ones you think are sensitive.

Mistake 5: Not testing from the attacker's perspective

RLS policies can have subtle bugs. Always test by making direct Supabase client calls without authentication (or with a different user's token) to verify that access is actually restricted.


RLS performance considerations

A common concern: doesn't RLS slow down queries?

In practice, the overhead is minimal. Supabase's RLS policies are compiled into the query plan as WHERE clauses. A policy like USING (auth.uid() = user_id) becomes WHERE user_id = 'uuid-here', which PostgreSQL handles efficiently — especially with an index on the user_id column.

For optimal performance:

-- Add indexes on columns used in RLS policies
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_org_members_user_id ON org_members(user_id);

The performance cost of RLS is negligible compared to the security cost of not having it.


The real-world consequences of skipping RLS

If this all feels abstract, here's what happens when RLS is missing:

  • Moltbook (January 2026): 1.5 million API tokens and 35,000 email addresses exposed. Full read/write access to every table. Fix time: 2 lines of SQL.

  • Lovable Platform (CVE-2025-48757): 170+ apps built without RLS. 303 vulnerable endpoints across tested applications. Personal debt amounts, home addresses, and API keys exposed.

  • Firebase Mass Misconfiguration (2024-2025): 900+ sites with equivalent test-mode security rules. 125 million user records exposed, including 19 million plaintext passwords.

Every one of these incidents would have been prevented by enabling RLS and writing basic access policies.


Your 15-minute action plan

  1. Right now (2 minutes): Run the RLS status query. Know where you stand.
  2. Next 10 minutes: Enable RLS on every table and add basic owner-access policies.
  3. Final 3 minutes: Test from an unauthenticated context. Make sure data isn't leaking.

That's it. Fifteen minutes to close the most common vulnerability in vibe-coded applications.

And if you want to verify everything is locked down — not just RLS, but your full security posture — that's what Flowpatrol is for. Sign up and scan your app in five minutes. If you're building with Lovable specifically, check out our step-by-step Lovable security guide.


RLS is a PostgreSQL feature. Everything in this article applies to any Supabase project regardless of which AI tool you used to build it. The Supabase RLS documentation covers every edge case and is worth reading alongside this guide.

Back to all posts

More in Security

IDOR: The Vulnerability AI Can't See
Mar 29, 2026

IDOR: The Vulnerability AI Can't See

Read more
The OWASP Top 10 Through the Lens of AI-Generated Code
Mar 29, 2026

The OWASP Top 10 Through the Lens of AI-Generated Code

Read more
SQL Injection Is Not Dead: How It Shows Up in AI-Generated Code
Mar 29, 2026

SQL Injection Is Not Dead: How It Shows Up in AI-Generated Code

Read more