vibecheck
ORIGINAL RESEARCH100 APPS · 6 WEEKS

We scanned 100 AI-built Next.js + Supabase apps.
31 leaked something they shouldn't.

S

Simon Lane

Founder, vibecheck · September 2026

I build the same way you do: prompt, deploy, move on. So I wanted to know how bad it actually is out there. Over six weeks I collected 100 publicly-deployed apps built with Cursor, Lovable, Bolt, v0, Replit, and Windsurf — from Show HN posts, Product Hunt launches, and the “I shipped this in a weekend” threads on X — and ran an unauthenticated scan against each one.

No logins. No exploitation. No data pulled. Only what any visitor's browser can already see: HTTP response headers, TLS configuration, CORS behaviour, cookie flags, and the JavaScript bundle that ships to every anonymous visitor.

Here's what came back.

The methodology

Collection criteria: apps had to be live (returning 200 on the root URL), built with a vibe coding tool (named in the post, README, or Product Hunt description), and running Next.js with Supabase as the backend. I excluded apps behind login walls, apps that redirected immediately, and anything that looked like a demo or a template rather than a real deployed product.

The scan itself was unauthenticated — I made the same requests any visitor's browser makes. I fetched the homepage and two or three additional routes, inspected the response headers, parsed cookies, and searched the JavaScript bundle for recognisable secret patterns: Supabase URL + key pairs, Stripe key prefixes, OpenAI key prefixes, Resend API keys, and other provider-specific formats. I also checked which tables the Supabase anon key could reach by attempting a select=* query on common table names (users, profiles, posts, orders, messages).

ℹ
Responsible disclosure note: I didn't pull any user data. For any app where I found a service_role key or an exposed table, I recorded the finding and moved on. I've reached out to the most severe cases privately.

The headline numbers

31/100
exposed at least one secret
in the client bundle or a public env var
12/100
shipped a Supabase service_role key
full read/write, RLS bypassed regardless
47/100
had a table readable with no RLS policy
visible to any anonymous visitor
58/100
had an unauthenticated API route or server action
reachable with no auth check
9/100
exposed a Stripe, OpenAI, or Resend key
directly in the JavaScript bundle
41s
median time to first finding
from first page load to first confirmed issue

To put 41 seconds in context: that's the time from opening the app in a browser, opening DevTools, and pressing Ctrl+Shift+F to search the bundle for SUPABASE_SERVICE_ROLE or sk_live_. For apps that exposed a secret, discovery was not a sophisticated operation.

Breaking it down

Secret exposure: the easiest win for an attacker (31/100)

The most common pattern was a Supabase key being passed to a client component via a NEXT_PUBLIC_ environment variable. This is correct for the anon key — but 12 of the 31 apps had done it with the service_role key. The service_role key bypasses every RLS policy you've written. It's the master key to your database.

Why does this happen? Because Lovable, Bolt, and Cursor often generate code that initialises the Supabase client directly in a component file. The developer grabs both keys from the Supabase dashboard, puts them in their env file, prefixes them with NEXT_PUBLIC_ so the component can read them, and deploys. The AI tool didn't warn them. The dashboard didn't warn them. The deploy didn't warn them.

The other 19 apps in the “exposed a secret” bucket were a mix: 9 had OpenAI, Stripe secret, or Resend API keys embedded in the bundle — directly callable by anyone who reads the source. The remaining 10 had other provider tokens (GitHub PATs, Anthropic keys, webhook secrets) that were less immediately dangerous but still a credential rotation exercise waiting to happen.

RLS: the Supabase trap nobody warns you about (47/100)

This was the most surprising number. Nearly half of all apps had at least one table where the anon key could run a SELECT * with no policy blocking it. In several cases that included a users table with email addresses, payment status, and subscription tier in plain text.

The misconception is understandable: developers assume the anon key is safe to make public because it's labelled “anon” and Supabase says it's fine to put in client-side code. That's true — with RLS enabled. Without RLS, the anon key is functionally a read-all key for your entire public schema. Supabase doesn't enable RLS by default when you create a table from the dashboard, and AI tools don't add it to generated SQL.

Of the 47 apps with open tables, 31 had a users or profiles table exposed. Several had order history. Two had a messages table with full conversation history readable anonymously. The data was there, unprotected, waiting.

Unauthenticated routes: auth that only lives on the frontend (58/100)

More than half the apps had at least one API route or server action that returned data or mutated state without verifying who was calling it. The frontend showed a login screen. The backend didn't enforce it.

This is the classic client-side-only auth mistake, and it's almost the default output from AI coding tools. When you prompt “make this page only visible to logged-in users”, the AI adds a redirect in the component. It doesn't add a session check to the API routes that component calls — because you didn't ask it to, and it doesn't know to infer it.

The most common unprotected routes were: /api/users, /api/profile, /api/dashboard, and /api/admin. Three apps had an admin route that returned a full user list with no auth check whatsoever.

What 41 seconds actually looks like

Here's the typical flow that produced a finding inside a minute. I open the app. I open DevTools. I go to the Network tab and reload. I look at the response headers — missing Content-Security-Policy and Strict-Transport-Security are already visible. Then I go to Sources, press Ctrl+Shift+F (search all files), type SUPABASE_SERVICE_ROLE. If it comes back with a result, I have the key.

For RLS I open a terminal and run: curl “https://[project].supabase.co/rest/v1/users?select=*” -H “apikey: [anon-key]”. If it returns rows, RLS is off on that table.

That's it. No tooling, no skill, no privilege. Anyone who reads this post can do this.

Why AI tools create these patterns

This isn't a criticism of the tools — they're genuinely excellent at generating working code fast. But “working” and “secure” are different targets, and the tools optimise for the former.

There are three structural reasons the same vulnerabilities appear repeatedly:

  1. AI models are trained on code that works, not code that is secure. The training set is full of tutorials, demos, and Stack Overflow answers that show the shortest path to a working example — which almost never includes RLS policies or auth middleware.
  2. Vibe coders don't iterate on security prompts. When a feature is broken, you reprompt until it works. Security issues don't break the feature — the app works fine with RLS off. So there's no feedback signal that anything is wrong.
  3. The stack creates invisible defaults. Supabase tables don't have RLS on by default. NEXT_PUBLIC_ variables don't warn you when you use them with secrets. Vercel deployments don't scan your bundle for credentials. Every step in the stack trusts you to know what you're doing.

The result is a large, growing surface of apps that are functional, often successful, and carrying vulnerabilities their founders don't know about.

What to do about it

The fixes are not difficult. Most of them take less time than the vulnerability took to introduce.

Enable RLS on every table →5 min per table
Audit your NEXT_PUBLIC_ variables →10 min
Add a session check to every API route →2 min per route
Add security headers to next.config.js →15 min, one-time

If you want to check your own app without doing this manually, vibecheck scans your GitHub repo automatically — running Gitleaks, Semgrep, njsscan, and 50 custom checks including RLS, unauthenticated routes, and NEXT_PUBLIC_ secrets. It's free for two repos and takes about 60 seconds.

FREE SCAN

Check your app in 60 seconds

Paste your URL for an instant header and TLS check — no account. Connect GitHub to scan the source code for secrets, RLS gaps, unauthenticated routes, and 45 more checks.

scan my app free →read the vulnerability guides

RELATED GUIDES

Supabase RLS DisabledCRITICALNEXT_PUBLIC Secret ExposureCRITICALLovable App SecurityAll 48 vulnerability guides →