The 3 AM Lovable panic — what to do when you see a leak tweet
It's 3 AM. You scrolled X and saw a tweet about a Lovable / Bolt / v0 app leaking customer data. You start wondering if yours is next. Here is the exact checklist to run in the next 30 minutes — what to check, what to fix first, and how to stop having this problem.
You built your app with Lovable, Bolt, v0, Cursor, or Replit. It works. People are using it. You went to bed feeling good about it.
Then you opened X and saw a thread about a vibe-coded app that leaked every customer's data through a missing Supabase RLS policy. The replies are full of "is yours next?" The thread has 12K likes.
Now it's 3 AM. You can't sleep. You don't know if your app has the same bug.
This is the checklist. Run it in the next 30 minutes. If you find the bug, you have a fix path. If you don't, you can sleep.
Step 1 — Pull up your Supabase project
Open your Supabase dashboard. Go to Authentication → Policies. You're going to look at every table.
If you see any table with the RLS Disabled badge, that's your problem. That table is readable by anyone with your anon key. The anon key ships in your client bundle by design — every visitor has it.
Fix in 60 seconds: click the table → Enable RLS. Then add at least one policy. The minimum-viable policy for a typical user-owned table is:
create policy "Users see their own rows" on public.your_table
for select using (auth.uid() = user_id);Repeat for insert, update, delete policies as needed. Default-deny by enabling RLS without a policy is a valid starting position — it locks the table while you write the right policies.
Step 2 — Check if your service-role key shipped to the client
This is the catastrophic version of the RLS bug. The service-role key bypasses every RLS policy you wrote. If it shipped client-side, your database is fully open regardless of your policy work.
How to check (60 seconds):
1. Open your repo in your editor.
2. Search for the literal string service_role and SUPABASE_SERVICE_ROLE_KEY.
3. For every match, ask: is this file rendered or imported by anything that runs in the browser?
In Next.js, the rule of thumb: anything in app/ or pages/ that does NOT have "use server" at the top, anything in components/ that doesn't, anything in lib/ that's imported by either — that code runs in the browser. The service-role key cannot live there.
If you find a service-role key in client code: rotate it immediately. Supabase dashboard → Settings → API → reset service_role key. Update your server-side env vars. Redeploy.
Step 3 — Check for keys in your git history
If a service-role key was in your repo at any point (even briefly, even in a commit you "fixed" later), assume it's compromised. GitHub's secret scanning catches some patterns; attackers' bots catch the rest.
git log -p --all -S 'eyJ' | head -200Scan that output for anything starting with eyJ followed by a long base64 string. That's a JWT pattern — every Supabase key starts with it. If you see a service-role key (the role claim in the decoded JWT will say service_role), rotate it.
Same for: OpenAI keys (sk-...), Stripe keys (sk_live_..., sk_test_...), GitHub PATs (ghp_...), AWS keys (AKIA...).
Step 4 — Check for the broken-auth bug everyone has
If your app has a route that returns user-specific data, there's a high chance the AI generated it without proper authorization (see the 92% data point for the underlying research). The pattern looks like:
// app/api/orders/[id]/route.ts
export async function GET(req, { params }) {
const order = await db.from('orders').select('*').eq('id', params.id).single();
return Response.json(order);
}The bug: any authenticated user can read any other user's order by changing the URL. The fix is comparing the order's owner against the authenticated user:
```ts export async function GET(req, { params }) { const { data: user } = await supabase.auth.getUser(); if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });
const order = await db.from('orders').select('*').eq('id', params.id).single(); if (order.data?.user_id !== user.id) { return Response.json({ error: 'not found' }, { status: 404 }); } return Response.json(order); } ```
(Return 404 not 403 — don't tell the attacker the resource exists.)
This bug class has names: BOLA (Broken Object Level Authorization), IDOR (Insecure Direct Object Reference). It's the most common bug in AI-generated apps. Search your routes for any params.id that hits the database without comparing ownership.
Step 5 — Check your middleware actually runs
If you have middleware.ts that's supposed to protect /api/admin/* or /api/private/*, verify the matcher actually matches. The common bug:
// middleware.ts
export const config = { matcher: '/admin/:path*' }; // matches /admin/...
// but your protected routes are at /api/admin/... — middleware doesn't run!Test it: curl https://your-app.com/api/admin/users from an incognito window. If the response is the protected data instead of a redirect or 401, your middleware is not protecting it.
Bonus check: make sure you're on Next.js 15.2.3+ (or 14.2.25+, 13.5.9+, 12.3.5+). If you're on an older version, you have CVE-2025-29927 — a critical middleware bypass via a single HTTP header. Upgrade now. Full write-up.
Step 6 — Stop having this problem
You can't run this checklist every time someone tweets about a leak. The whole point of vibe-coding is that you don't want to think about security every day.
Securie is the autonomous security engineer for your AI-built app. Install the GitHub App in 2 minutes. On every PR, three Day-1 specialists run automatically:
- Supabase RLS specialist — reads every
create policystatement, runs your policies in a sandbox, tells you which ones leak rows - Secret scanner — detects every leaked credential pattern, live-validates against the provider API, and (Indie tier and up) opens an auto-rotation PR
- Broken auth specialist — reads every route that uses
params.id, runs a cross-user request in a sandbox, tells you which ones return another user's data
Findings only ship if the sandbox successfully reproduces the exploit. No noise, no triage, no false positives.
After tonight
Go back to bed. Tomorrow morning, install Securie and let it run on your codebase. You will probably find one or two issues you missed. That's fine — that's the point. The bugs you fix the morning after the panic are the bugs that don't get tweeted about in a thread with 12K likes.
Related
Related posts
From a growing sample of publicly-reachable Supabase projects we've audited, the same seven mistakes come up every time: RLS off on at least one table, service-role key in the client, missing tenant scoping, default-allow policies, no policies on storage buckets, exposed JWT secret, and over-broad anon-role grants. Fixes for each.
Moltbook leaked 1.5 million API keys, 35,000 emails, and 4,060 private messages in 72 hours. Wiz's disclosure showed the root cause: a single Supabase table without row-level security. Here is the timeline, the exact bug, and the ten-minute hardening walkthrough for your own app.
Supabase and Firebase are the two backend defaults for AI-built apps. Here is the honest comparison — what each is best at, where each one's bugs hurt most, and which one to pick for your specific stack.
Most auth tutorials show you how to add a login button. This is the guide that shows you how to add auth that actually works — what to wire up, what AI tools get wrong, and the bugs you ship if you copy-paste the first Stack Overflow answer.