A Security Checklist for Vibe-Coded SaaS Apps
AI coding tools produce code that runs, not code that's secure — and they fail in predictable places: Row Level Security left off, the service_role key exposed, no input validation, and secrets in config. Before real users touch a vibe-coded Next.js + Supabase app, walk this checklist: enable RLS on every table, keep service_role server-side, validate every API input, and get secrets out of your code and configs.
Lovable, Cursor, v0, and Claude are genuinely good at getting a SaaS app working. They are not trying to make it secure — security rarely shows up in the prompt, so it rarely shows up in the output. The result is a class of apps that demo perfectly and leak on day one. This is the hardening pass to run before launch. Each item links to a deep-dive with the exact fix.
This is a documented pattern, not a scare story — CVE-2025-48757. In May 2025, researcher Matt Palmer found 303 endpoints across 170 production apps exposing user data through Supabase's public anon key because Row Level Security wasn't enabled — the exact failure AI builders keep shipping. The official record scores it CVSS 9.3.
1. Turn on Row Level Security — everywhere
This is the number-one vibe-coded leak. On Supabase, every public table is reachable through the auto-generated API with the anon key that ships in the browser; RLS is the only gate. AI tools frequently create tables and never enable it, or disable it to "fix" a blocked query.
- RLS enabled on every table in
public(see the RLS complete guide) - Policies scoped to
auth.uid(), notusing (true)or a bareauthenticatedcheck - Confirmed no table was created without a policy
2. Keep the service_role key server-side
The service_role key bypasses RLS entirely. AI tools love reaching for it because it makes everything "just work" — including handing it to the browser via a NEXT_PUBLIC_ prefix, which is a full database compromise.
-
service_rolenever behind aNEXT_PUBLIC_prefix - Secrets read only in
server-onlymodules (keeping secrets out of Next.js) - The agent/MCP server is not handed
service_role(see below)
3. Don't give your AI agent the keys to the database
If you wired an MCP server into your editor, check its config. Handing the agent the service_role key means any prompt injection can read or modify your whole database — the "lethal trifecta." Here's GuardLayer's live output on a config that gives the Supabase agent service_role:
- Critical.cursor/mcp.json:7
service_role key exposed to an AI agent
Never hand an agent the service_role key. Give it a scoped key / a dedicated DB role with RLS enforced, and put any privileged action behind a narrow tool the agent can't be tricked into calling with arbitrary arguments. - Critical.cursor/mcp.json:7
service_role key exposed to an AI agent
Never hand an agent the service_role key. Give it a scoped key / a dedicated DB role with RLS enforced, and put any privileged action behind a narrow tool the agent can't be tricked into calling with arbitrary arguments.
- Agents get a scoped key or an RLS-enforced role, never
service_role - No secrets pasted into MCP config — use
${env}references
4. Validate every input and guard every endpoint
Generated API routes and Server Actions tend to trust their input and skip auth. Both are public surfaces.
- Every API route validates its body against a schema (zod
safeParse) and returns 400 on bad input - Every Server Action resolves and checks the user before mutating data — remember Server Actions are public endpoints, callable by anyone
- No raw string interpolation into SQL queries
5. Get secrets out of source and history
- No hardcoded API keys in source
-
.env*in.gitignore; anything already committed is rotated, not just deleted - Built bundle checked for leaked tokens (
grep -r "service_role" .next/)
Can I trust a scanner to make my vibe-coded app secure?
Partly — and it's worth being precise about the boundary. A static scanner like GuardLayer catches the mechanical mistakes on this list: RLS off, exposed keys, missing input validation, secrets in config. It runs on every push and gives you the exact fix. What it cannot do is reason about your business logic, test your app at runtime, or stop prompt injection — no static tool can. So a scanner is the fast first pass that clears 80% of the predictable holes; the remaining architectural decisions (what access your agent has, what your policies actually intend) are still yours. Treat "the scan is clean" as necessary, not sufficient.
FAQ
Is code from Lovable / Cursor / v0 insecure? Not deliberately — but it optimizes for working, not secure. The common gaps are missing RLS, an exposed service_role key, and unvalidated input. Run this checklist before launch and most are quick fixes.
What's the single most important item? Row Level Security. On Supabase it's the gate between an anonymous request and your data, and it's the most frequently skipped step. Start there (the complete guide).
Can a static scanner detect prompt injection in my AI app? No. Static analysis reads code and config; prompt injection is a runtime behavior of the model. A scanner catches the secrets and over-privileged keys that make an injection catastrophic — it doesn't detect the injection itself.
I'm the only developer. Do I still need this? Yes. These holes are exploited by anyone on the internet with your app's public anon key, regardless of team size. Solo apps get scraped the same as funded ones.
Catch this before it ships — free
GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.
No signup, no card — your code is scanned in memory and never stored.