← All posts
May 1, 2026supabaserlsdatabaselovablebolt

Supabase RLS: What Every Vibe Coder Gets Wrong

65% of AI-built apps have misconfigured RLS. The dashboard shows 'enabled' — but USING (true) means your data is open to everyone. Here's how to fix it.

Row Level Security is Supabase's mechanism for preventing one user from reading another user's data. When it works correctly, a query from user A returns only rows that belong to user A — even if the query itself does not include a WHERE user_id = ? clause.

When it is misconfigured, every user can read every row.

65% of AI-built apps have misconfigured RLS — not disabled, misconfigured. The Supabase dashboard shows "RLS enabled" with a green indicator. The code passes review. The app works. And every row in the database is accessible to every authenticated user, or in some cases, to anonymous requests.


Why AI tools get this wrong

AI tools generate syntactically correct SQL. The problem is semantic — the meaning of the policy.

A freshly scaffolded Lovable or Bolt app often contains policies like this:

-- This policy sounds right
CREATE POLICY "Enable read access for all users"
  ON public.notes
  FOR SELECT
  USING (true);

The name says "all users." The intent was probably "all authenticated users who own the note." But USING (true) evaluates to true for every row, for every request, including unauthenticated ones on tables that allow anon access.

This is not an edge case. It is the default output of AI code generation because the simplest working policy — "let users read data" — resolves to USING (true) without additional context about user ownership.


The correct pattern

For any table where rows belong to a specific user, the policy should check the authenticated user's ID against the ownership column:

-- Select: user sees only their own rows
CREATE POLICY "Users can read own rows"
  ON public.notes
  FOR SELECT
  USING (auth.uid() = user_id);

-- Insert: user can only insert rows they own
CREATE POLICY "Users can insert own rows"
  ON public.notes
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Update: user can only update rows they own
CREATE POLICY "Users can update own rows"
  ON public.notes
  FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Delete: user can only delete rows they own
CREATE POLICY "Users can delete own rows"
  ON public.notes
  FOR DELETE
  USING (auth.uid() = user_id);

auth.uid() returns the UUID of the currently authenticated user from the Supabase JWT. If no user is authenticated, it returns null, which never equals a real user_id — so anonymous requests get nothing.


How to audit your existing policies

In the Supabase dashboard, go to Authentication → Policies. For every table that contains user data, read each policy's USING clause.

The dangerous patterns:

USING (true)                    -- Open to everyone, always
USING (auth.role() = 'authenticated')  -- Any logged-in user sees all rows
USING (auth.uid() IS NOT NULL)  -- Same problem as above

The safe pattern:

USING (auth.uid() = user_id)    -- User sees only their rows

If your table uses a different column name for ownership (e.g., owner_id, created_by, profile_id), substitute accordingly.


The multi-tenant case

If your app has organizations or teams where multiple users share access to the same data, the pattern extends:

-- User can read rows where they are a member of the owning organization
CREATE POLICY "Team members can read org data"
  ON public.documents
  FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id
      FROM public.memberships
      WHERE user_id = auth.uid()
    )
  );

This is more complex but follows the same principle: access is granted based on a verifiable relationship between the requesting user and the row, not a blanket condition.


What the April 2026 Lovable incident actually was

CVE-2025-48757 was a backend regression — a code change on Lovable's infrastructure that re-enabled public project access. This was not a user misconfiguration. But the underlying exposure (project data readable by other users) is structurally identical to what open RLS policies cause in user-built apps.

The lesson is the same: "RLS enabled" on the dashboard is not the same as "data is protected." You have to verify what each policy actually permits.


Verify before you launch

Paste your deployed app URL into VibeScan. The URL scan checks your Supabase public surface — whether your project is exposing data through the anon key without appropriate restrictions.

For a deeper check on your actual RLS policies, go to Supabase Dashboard → Authentication → Policies and read every USING clause on every table that contains user data.


Sources: CVE-2025-48757 (Lovable, April 2026); PreBreach OWASP Top 10 in AI-Generated Code (Feb 2026); Supabase Row Level Security documentation.

Check your own app

Free scan — no GitHub access needed. Takes 30 seconds.

Scan my app free