← All case studies
RESCUEauth

Lovable App Auth Bypass: When Your Admin Dashboard Is Protected by a React State Boolean

2026-08-01 · RESCUE

This is the archetypal failure mode of vibe-coded apps: the tool (Lovable, Bolt, v0, etc.) gives you a working UI fast, but the invisible parts — authentication, server-side enforcement, environment isolation — are left to the builder. When they're skipped, the cracks form within weeks.

Here's a real failure pattern we see across platforms, documented on a representative Lovable-generated app.


The Platform: Lovable

Lovable generates full-stack apps with a Supabase backend. It scaffolds authentication via Supabase Auth, schema via supabase/ migrations, and a React + TypeScript frontend. The result is a working app — login, dashboard, CRUD — in minutes.

But Lovable, like all vibe-coding tools, operates on visible instructions: it builds what you tell it to build. It does not automatically add Row Level Security, server-side middleware, or API key rotation — because nobody prompted it to.


What Failed: The Client-Side Auth Guard

A typical Lovable app has an admin page at /admin. After the user logs in with their email and password, the app renders an AdminDashboard component — but only if a React state variable says the user is authenticated:

// This is the pattern Lovable generates — and it's the bug
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);

// After Supabase login:
setIsAuthenticated(true);
setUser(session.user);

// The "guard":
{isAuthenticated ? <AdminDashboard /> : <LoginPage />}

Three things are wrong here:

  1. The guard is client-side only. Anyone who inspects the bundle can see every route, every component, every Supabase query. The admin dashboard code — including the names of all database tables and columns — ships to every visitor.

  2. Supabase credentials are in the bundle. Lovable embeds supabaseUrl and supabaseAnonKey in the frontend. The anon key is public by design, but without Row Level Security (RLS), it grants full read access to every table. That means SELECT * FROM users works from the browser console.

  3. There is no server-side authorization check. The admin page makes Supabase queries directly from the browser. If RLS policies don't restrict by auth.uid(), the queries succeed regardless of who runs them.

What Gets Exposed

In the diagnosed app, these were all accessible from the browser console:


Diagnosis: How to Tell If Your App Has This

You don't need access to the code. Four checks from the browser:

1. View the JavaScript bundle

Open DevTools → Sources → look for supabaseUrl and supabaseAnonKey. If you see them in a .js file that loads before authentication, the anon key is public. This is normal — but it means RLS is your only defense.

2. Try an unauthenticated query

Open the browser console on the login page and paste:

// Replace with the values from the bundle
const supabase = window.supabase; // or create a client
supabase.from('users').select('*').then(console.log);

If data comes back, RLS is off. Every table is a public API.

3. Check if /admin loads before login

Navigate directly to https://yourapp.com/admin while logged out. If you see a flash of the admin layout before the redirect fires, the page renders before the auth check resolves — leaking data in a race condition.

4. Inspect the network tab

Log in as a normal user, then manually change the URL to an admin endpoint. Watch the Network tab: if the request succeeds with status 200 and returns admin-only data, there's no server-side authorization.


The Fix: Three Layers

The repair is straightforward but requires touching code Lovable doesn't touch by default:

Layer 1 — Supabase Row Level Security

Enable RLS on every table that holds user data, then add policies:

-- Enable RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE admin_notes ENABLE ROW LEVEL SECURITY;

-- Users can only read their own row (unless they're admin)
CREATE POLICY "users_read_own" ON users
  FOR SELECT USING (auth.uid() = id);

-- Admin-only reads
CREATE POLICY "admin_notes_read_admin" ON admin_notes
  FOR SELECT USING (
    auth.uid() IN (SELECT id FROM users WHERE role = 'admin')
  );

This is the minimum viable fix. Without it, the anon key is a skeleton key.

Layer 2 — Move Sensitive Logic to Edge Functions

Supabase queries that touch admin data should never run from the browser. Move them to Supabase Edge Functions (Deno):

// supabase/functions/admin-get-users/index.ts
import { serve } from 'https://deno.land/std/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const authHeader = req.headers.get('Authorization');
  // Verify the JWT server-side
  const supabase = createClient(/* ... */);
  const { data: { user } } = await supabase.auth.getUser(authHeader.replace('Bearer ', ''));

  if (!user) return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });

  // Check admin role
  const { data: profile } = await supabase.from('users').select('role').eq('id', user.id).single();
  if (profile?.role !== 'admin') return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 });

  // Now safe to fetch
  const { data } = await supabase.from('users').select('*');
  return new Response(JSON.stringify(data), { status: 200 });
});

Now the admin dashboard calls this endpoint instead of querying Supabase directly. The anon key in the bundle remains public, but it no longer grants access to sensitive data — RLS gates direct queries, and Edge Functions gate admin logic.

Layer 3 — Route-Level Protection

Add a proper auth middleware. In Next.js or a simple Express wrapper behind the app:

// middleware.ts (Next.js example)
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';

export async function middleware(req) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });
  const { data: { session } } = await supabase.auth.getSession();

  if (req.nextUrl.pathname.startsWith('/admin') && !session) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  return res;
}

This prevents the admin page HTML from even being served to unauthenticated users — fixing the flash/race condition.


Cost: What This Repair Looks Like at RESCUE Prices

Phase What's delivered Price
Diagnosis Written report: which tables lack RLS, which queries are exposed, what's in the bundle, step-by-step fix plan. Delivered in 72 h from code access. €149
Emergency Fix All three layers applied: RLS + Edge Functions + route protection. Delivered as a PR + 2-minute screen recording. One bug scope (auth bypass). 5 working days. €590
Security Pass Emergency Fix scope + audit of all RLS policies, API key rotation, environment variable cleanup, CSP headers, and a hardened CI check that blocks commits with secrets in the bundle. 7 working days. €1,290

These are fixed prices — no hourly billing, no calls. The diagnosis stands alone: you keep the report whether or not you hire the repair.


Why This Keeps Happening

Vibe-coding tools optimize for the visible: the UI, the happy path, the demo that gets a "wow" on Twitter. They don't optimize for the invisible — authorization, data isolation, attack surface — because invisible things don't generate prompts.

The fix isn't to stop using AI to build. It's to recognize that AI accelerates the visible 80% and leaves the invisible 20% exactly where it was: on you. RESCUE exists for that 20%.

Have a similar problem?

We build and fix production systems at a fixed price.

Talk to FIRME