Fairy
Resources

How to Avoid: Supabase Table Without RLS

August 9, 2026 · 8-minute read · Fairy

The short answer

To prevent Supabase tables without RLS in AI-generated code, always enable Row Level Security on every table containing user data, then add explicit policies defining who can read and write. AI often creates tables without RLS because training data includes tutorials that skip security for simplicity. Catch this by auditing every CREATE TABLE statement and verifying RLS is enabled with proper policies before deployment.

The Direct Answer: Enable RLS and Add Explicit Policies

To prevent Supabase tables without Row Level Security (RLS) in AI-generated code, you need two things for every table containing user data:

  1. Enable RLS on the table with ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
  2. Add explicit policies defining who can SELECT, INSERT, UPDATE, and DELETE rows

AI frequently generates tables without either step. The result: every row in your database is readable and writable by any user who can reach your Supabase client.

This isn't a theoretical risk. When RLS is disabled, your anon key (which is public and visible in client-side code) grants full access to the table. A user can open their browser console, craft a query, and read every row in your users, payments, or messages table.

Why AI Produces This Bug

AI models learn from the code they've seen. And the code they've seen—tutorials, Stack Overflow answers, GitHub examples—often skips RLS for the sake of brevity.

Consider the typical "Getting Started with Supabase" tutorial:

CREATE TABLE todos (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  task TEXT NOT NULL,
  completed BOOLEAN DEFAULT FALSE
);

This works. The tutorial moves on to querying data. RLS isn't mentioned because it would complicate the lesson.

When you prompt an AI to "create a todos table in Supabase," it reproduces this pattern. The AI successfully creates a functional table—which is exactly what you asked for. The security gap isn't a hallucination or a random error. It's a faithful reproduction of incomplete patterns from training data.

The Training Data Problem

Three factors make this worse:

  1. Tutorial bias: Educational content optimizes for clarity, not production readiness. Security steps are "left as an exercise."

  2. Working code bias: AI doesn't distinguish between "this code runs" and "this code is safe." Both patterns exist in training data; the simpler one wins.

  3. Context blindness: AI doesn't know your table contains user data. It sees a schema request and generates a schema. The semantic meaning—"this is private user information"—doesn't trigger security considerations.

The Specific Danger: Why This Is Critical

When RLS is disabled on a table:

The anon key is meant to be public. It's in your JavaScript bundle. Supabase's security model assumes RLS will restrict what that key can do. Without RLS, the assumption breaks.

Here's what an attacker sees in your browser's network tab:

// Your client-side code
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

// What an attacker can do with that same key
const { data } = await supabase.from('users').select('*');
// Returns EVERY user in your database

This compounds with other AI-generated security flaws. If the same codebase has authentication bypasses or exposes the service role key client-side, the damage escalates further.

How to Detect Missing RLS

In SQL Migrations

Search your migration files for CREATE TABLE statements that lack corresponding RLS statements:

-- RED FLAG: Table created without RLS
CREATE TABLE profiles (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  display_name TEXT
);

-- CORRECT: RLS enabled with policies
CREATE TABLE profiles (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  display_name TEXT
);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own profile"
  ON profiles FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  USING (auth.uid() = user_id);

In the Supabase Dashboard

  1. Navigate to Database > Tables
  2. For each table, check if "RLS Enabled" is shown
  3. Click into the table and verify policies exist under "Policies"

A table with RLS enabled but no policies is effectively locked—no one can access it. This is another common AI failure: enabling RLS without understanding that policies are required.

Automated Verification

Query the system catalogs to find tables without RLS:

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

For tables with RLS enabled, verify policies exist:

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

If a table has rowsecurity = TRUE but no entries in pg_policies, users will get permission denied errors—or in some configurations, see no data at all.

The Correct Pattern

Every user-data table needs both RLS and policies. Here's the complete pattern:

-- 1. Create the table
CREATE TABLE messages (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  sender_id UUID REFERENCES auth.users(id) NOT NULL,
  recipient_id UUID REFERENCES auth.users(id) NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 2. Enable RLS (required)
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- 3. Add explicit policies (required)
-- Users can read messages they sent or received
CREATE POLICY "Users can view own messages"
  ON messages FOR SELECT
  USING (auth.uid() = sender_id OR auth.uid() = recipient_id);

-- Users can only insert messages as themselves
CREATE POLICY "Users can send messages"
  ON messages FOR INSERT
  WITH CHECK (auth.uid() = sender_id);

-- Users can delete messages they sent
CREATE POLICY "Users can delete sent messages"
  ON messages FOR DELETE
  USING (auth.uid() = sender_id);

Key Principles

  1. Default deny: If no policy matches, the operation is denied. This is safe by default.

  2. Separate policies per operation: SELECT, INSERT, UPDATE, DELETE can have different rules. Don't assume one policy covers all.

  3. Use auth.uid(): This returns the authenticated user's ID from the JWT. It's the foundation of user-scoped policies.

  4. USING vs WITH CHECK: USING filters which rows are visible. WITH CHECK validates new/modified data. For INSERT, use WITH CHECK. For SELECT/DELETE, use USING. For UPDATE, you often need both.

Common Variations and Edge Cases

Service Role Key Bypass

RLS doesn't apply when you use the service role key. This is by design—server-side operations need full access. But AI sometimes puts the service role key in client-side code:

// CRITICAL BUG: Service role key in client code
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY  // Bypasses ALL RLS
);

If you see NEXT_PUBLIC_ prefixed on a service role key, or any service role key in client-side code, that's a critical security flaw that makes RLS irrelevant. The service role key must be server-only.

Public Tables

Some tables genuinely should be public. A countries lookup table or product_catalog might be world-readable. In these cases:

ALTER TABLE countries ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can read countries"
  ON countries FOR SELECT
  USING (TRUE);

-- No INSERT/UPDATE/DELETE policies = only service role can modify

Enabling RLS with a permissive SELECT policy is safer than disabling RLS entirely. You still control writes.

RLS Without Policies (The Deny-All Surprise)

AI sometimes enables RLS but forgets policies:

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- No policies created

The result: nobody can read or write the table (except with the service role key). This manifests as "data not loading" bugs that confuse developers.

Always verify policies exist after enabling RLS.

Integrating Into Your Workflow

Before Deployment

Run verification queries against your database schema. Every table in the public schema should either:

In Code Review

When reviewing AI-generated Supabase code, check:

  1. Every CREATE TABLE has a corresponding ENABLE ROW LEVEL SECURITY
  2. Every RLS-enabled table has policies for each operation it needs
  3. No service role keys appear in client-side code
  4. Policies use auth.uid() correctly to scope access

With AI Code Review Tools

Automated AI code review can catch missing RLS declarations by pattern-matching against CREATE TABLE statements and verifying corresponding security statements exist. This catches the bug before human review.

For more complex policy verification—ensuring the policies actually enforce the intended access patterns—expert code review provides the judgment that automated tools cannot.

The Broader Pattern: AI and Security Assumptions

This failure mode reveals a general truth about AI-generated code: AI reproduces patterns without understanding context. Security is almost always contextual. A table schema is correct or incorrect based on what data it holds and who should access it—information the AI doesn't have.

When you prompt "create a users table," the AI doesn't know:

It generates the syntactically correct, functionally complete, and security-absent code it learned from tutorials.

The fix isn't to stop using AI for database code. The fix is to verify security properties separately from functionality. Continuous oversight catches these gaps systematically rather than relying on human reviewers to remember every security check.

Summary

Supabase tables without RLS are a critical and common failure mode in AI-generated code. The AI learns from tutorials that skip security, then reproduces those patterns in your production codebase.

Prevention requires:

  1. Enable RLS on every table with user data
  2. Add explicit policies for SELECT, INSERT, UPDATE, DELETE as needed
  3. Verify both steps in code review
  4. Keep service role keys server-side only
  5. Audit existing tables with system catalog queries

The security model Supabase provides is sound—but only when enabled. AI won't enable it for you.

Frequently asked questions

What happens if a Supabase table doesn't have RLS enabled?

Without RLS enabled, every row in the table is readable and writable by any authenticated or anonymous user with access to your Supabase client. This means any user can read all data, modify records belonging to others, or delete entire tables.

Why does AI-generated code often miss RLS?

AI models learn from tutorials and examples that prioritize functionality over security. Many Supabase quickstarts skip RLS for simplicity, teaching AI that tables work without it. The AI completes the task (data storage) without understanding the security context.

Is enabling RLS enough to secure my table?

No. Enabling RLS without adding policies creates a deny-all state where no one can access the data. You must enable RLS AND add explicit policies that define read/write permissions. This two-step requirement is a common source of bugs.

How do I check if my Supabase tables have RLS enabled?

Query the pg_tables and pg_policies system catalogs, or check the Supabase dashboard under Database > Tables. For each table, verify both that RLS is enabled (ALTER TABLE ... ENABLE ROW LEVEL SECURITY) and that appropriate policies exist.


Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.

More resources