In January 2026, security researchers at Hacktron AI disclosed CVE-2025-48757 [1]. Over 170 applications built with Lovable (a popular "vibe-coding" AI tool) had their databases wide open. One breach alone exposed 13,000 users through a publicly queryable password reset token table [2].
The root cause wasn't sophisticated. No zero-day exploits. No advanced persistent threats. Just missing Row Level Security (RLS) policies on Supabase tables, turning the publicly available anon API key into a master key for the entire database.
This wasn't an isolated incident either. A scan of 20,000+ indie launch URLs found that 11% exposed Supabase credentials in their frontend code [3]. 83% of all exposed Supabase databases stem from RLS misconfigurations [4]. The pattern is clear, and it's fixable.
Table of Contents
- What Actually Happened
- How Supabase Security Works
- The Five Mistakes That Get You Hacked
- The Security Checklist
- Real RLS Policies You Can Use Today
- The Backend-Only Alternative
- Tools for Auditing Your App
- Ship Fast, But Ship Secure
What Actually Happened
Lovable generates full-stack apps from natural language prompts. It spins up a separate Supabase instance for each project, creates tables, and writes the frontend code to query them [7]. The problem: it wasn't enabling RLS or writing policies for those tables.
Supabase auto-generates REST APIs from your PostgreSQL schema [6]. Without RLS, anyone who knows your project URL and anon key (both of which are embedded in client-side JavaScript) can query any table directly.
Attackers didn't need anything fancy. A single curl command was enough:
curl "https://your-project.supabase.co/rest/v1/users?select=*&limit=1000" \
-H "apikey: your-anon-key"
That returns every row in the users table. Names, emails, metadata, everything. In one documented case, a password reset token table was exposed, enabling mass account takeovers [1].
How Supabase Security Works
Supabase gives you two API keys:
anonkey is a public key embedded in client-side code. It's meant to be used with RLS policies that restrict what data each user can access.service_rolekey is a server-only key that bypasses RLS entirely. If this hits the browser, your database is fully exposed regardless of any policies.
Row Level Security (RLS) is the mechanism that makes the anon key safe. It lets you define SQL policies that control which rows each user can read, insert, update, or delete. Without policies, RLS-enabled tables deny all access. Without RLS enabled at all, tables allow full access to anyone.
The critical detail: RLS is disabled by default on tables created via SQL [6]. Tables created through the Supabase dashboard have it enabled by default, but AI tools like Lovable use SQL migrations.
The Five Mistakes That Get You Hacked
These are the patterns that keep showing up in breached Supabase apps:
- No RLS on tables. The number one cause. AI-generated code creates tables without enabling RLS, and developers ship without checking.
service_rolekey in the frontend. AI boilerplate and copied tutorials regularly expose this key. Check your environment variables. If it's prefixed withNEXT_PUBLIC_,VITE_, orPUBLIC_, it's in the browser bundle.- RLS enabled but no policies. Enabling RLS without writing policies blocks everything, which makes developers disable it "temporarily." That temporary fix ships to production.
- Testing in the SQL editor. The Supabase SQL editor bypasses RLS. Your policies might be broken, but you'd never know because your test queries succeed.
- Trusting AI-generated security code. A December 2025 audit by Tenzai tested five major vibe-coding tools. They found 69 vulnerabilities across 15 applications [3]. Every single tool shipped insecure code.
The Security Checklist
Here's the concrete steps to lock down your Supabase database.
Enable RLS on every table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY;
Run this for every table in your public schema. No exceptions.
Write explicit policies
Every table needs at least one policy per operation. Here's a standard pattern for user-owned data:
-- Users can only read their own profile
CREATE POLICY "Users read own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Users can only update their own profile
CREATE POLICY "Users update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
Audit your environment variables
Make sure the service_role key is never exposed client-side. Run this in your project root:
grep -r "service_role\|supabase_service" --include="*.ts" --include="*.tsx" \
--exclude-dir=node_modules --exclude-dir=.next
Any match in a file that doesn't have 'use server' or isn't in an API route is a problem.
Test from the client, not the SQL editor
The SQL editor uses a privileged connection that bypasses RLS. Always test your policies using the actual anon key:
# This should return an empty array or only your own data
curl "https://your-project.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: your-anon-key" \
-H "Authorization: Bearer your-anon-key"
If you see other users' data, your policies are broken.
Rotate keys if exposed
If your service_role key has ever been in client-side code, even briefly, rotate it immediately in the Supabase dashboard under Settings > API.
Real RLS Policies You Can Use Today
Here are production-ready policies for common SaaS tables:
User profiles
-- Public read for display names only
CREATE POLICY "Public profiles are viewable"
ON profiles FOR SELECT
USING (true);
-- Users can only modify their own profile
CREATE POLICY "Users update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
Private user data
-- Strict isolation: users see only their own data
CREATE POLICY "Users read own data"
ON user_settings FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users insert own data"
ON user_settings FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users update own data"
ON user_settings FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Purchases (read-only for users, write via server)
-- Users can view their own purchases
CREATE POLICY "Users read own purchases"
ON purchases FOR SELECT
USING (auth.uid() = user_id);
-- No INSERT/UPDATE/DELETE policies: writes happen
-- server-side with the service_role key
Index the columns referenced in your policies. Missing indexes on RLS columns is the top performance killer:
CREATE INDEX idx_user_settings_user_id ON user_settings(user_id);
CREATE INDEX idx_purchases_user_id ON purchases(user_id);
The Backend-Only Alternative
RLS policies are a solid defense layer, but they add complexity, especially as your app grows. Every new table needs policies. Every policy needs testing. One missed table and you're back to square one.
There's another approach: move all database access to the server and skip client-side queries entirely.
This is how SecureStartKit handles it. All database queries go through Server Actions using the service_role key server-side. The browser never talks to Supabase directly. There's no anon key in the client bundle, no auto-generated REST API to worry about, and no RLS policies to misconfigure.
'use server'
import { createAdminClient, getUser } from '@/lib/supabase/server'
import { z } from 'zod'
const schema = z.object({ name: z.string().min(1).max(100) })
export async function updateProfile(data: z.infer<typeof schema>) {
const parsed = schema.safeParse(data)
if (!parsed.success) return { error: 'Invalid input' }
const user = await getUser()
if (!user) return { error: 'Unauthorized' }
const admin = createAdminClient()
await admin
.from('profiles')
.update({ full_name: parsed.data.name })
.eq('id', user.id)
}
Every mutation is validated with Zod, authenticated server-side, and scoped to the current user. The service_role key stays on the server. The attack surface shrinks to zero.
RLS still serves as defense-in-depth. Tables deny all access to anon by default. But the architecture doesn't depend on getting policies right.
Tools for Auditing Your App
- Supabase Security Advisor is built into the dashboard [5]. Scans for missing RLS, unindexed policy columns, and other misconfigurations. Run it before every deploy.
- SupaExplorer is a free OAuth-based audit tool that visualizes your RLS policies and generates SQL fixes.
- VibeAppScanner was specifically built after the Lovable incident [4]. Tests your app's Supabase exposure from the outside.
- pgTAP is a PostgreSQL testing framework. Write automated tests for your RLS policies and run them in CI.
Ship Fast, But Ship Secure
Vibe-coding tools aren't going away, and they shouldn't. Shipping fast is a competitive advantage. But "fast" doesn't have to mean "insecure."
The Lovable incident exposed a pattern that goes beyond one tool. AI-generated code routinely skips security fundamentals. If you're using any AI tool to build your app (Lovable, Cursor, Replit, or anything else) you need to verify the security layer yourself.
Enable RLS on every table. Write explicit policies. Keep the service_role key on the server. Test from the client. Or better yet, move all data access to the backend and remove the attack surface entirely.
Your users' data depends on it.
References
- SupaPwn: Hacking Our Way into Lovable's Office and Helping Secure Supabase— hacktron.ai
- Supabase Security Flaw: 170+ Apps Exposed by Missing RLS— byteiota.com
- Vibe Coding Cybersecurity Insight Report, January 2026— supaexplorer.com
- Supabase Row Level Security (RLS): Complete Guide 2026— vibeappscanner.com
- Supabase Security Retro: 2025— supabase.com
- Row Level Security, Supabase Docs— supabase.com
- The Vibe Coding Stack: When AI-Driven Speed Becomes Your Biggest Liability— medium.com
Related Posts
Why Security-First Matters for Your SaaS
Most SaaS templates expose your database to the browser. Here's why that's dangerous and how SecureStartKit does it differently.
The Modern SaaS Stack: Next.js 15 + Supabase + Stripe
Why Next.js 15, Supabase, and Stripe make the ideal stack for building SaaS products in 2025.
Getting Started with SecureStartKit
Set up your SecureStartKit SaaS template in under 10 minutes. Clone, configure, and deploy.