Vibe Coding Security Checklist: 12 Checks Before You Launch
This vibe coding security checklist is for an app built with Lovable, Bolt, Cursor, v0, Replit or Claude Code that is about to meet real users. It has twelve items, ordered by how much damage each miss does, and every item comes with a check you can run in about two minutes and a fix. Work from the top: a miss in any of the first three can expose every user's data or your keys at once.
Are vibe coded apps safe?
They can be. The assistant writes code that works, and working is not the same as closed. The usual vibe coding security risks are a short list: database rules left open, secret keys shipped to the browser, checks done only in the UI, and webhooks nobody verifies. Each one is quick to check yourself.
That is most of what vibe coding security best practices come down to. None of this needs a security background. It needs a terminal, your browser's developer tools, and an hour.
The vibe coding security checklist at a glance
| # | Item | Why it matters | How to check |
|---|---|---|---|
| 1 | Database rules (RLS) | Anyone can read every user's rows | One SQL query |
| 2 | Secret keys in frontend code | Keys ship to every visitor | Grep the build output |
| 3 | Keys in git history | Deleted is not gone | git log or gitleaks |
| 4 | Server-side authorization | Hidden buttons stop no one | Replay a request with another id |
| 5 | Payment webhooks verified | Fake "paid" events | Grep for the signature check |
| 6 | Exploited packages | Known attacks, already in use | npm audit against CISA's list |
| 7 | AI keys and model output | Bills, XSS, injection | Grep for browser clients and raw HTML |
| 8 | Admin and debug routes | Free admin panel | List your routes, open them logged out |
| 9 | Cron endpoints | Anyone can run the job | Read each cron handler |
| 10 | Backups and restore | One bad migration ends it | Find your last backup |
| 11 | Error messages | Stack traces map your code | Send a broken request |
| 12 | Security headers and CSP | Easier XSS and clickjacking | curl -I your domain |
If you'd rather start with a scan, a free scan of your GitHub repo (no account needed) flags several of these: Supabase RLS shapes in your .sql migrations, secrets committed to the repo including its git history, packages attackers are already exploiting (from CISA's catalog, refreshed weekly), Stripe webhook handlers with no signature check, model output reaching HTML, SQL or a shell, an AI client running in the browser with its key in a public env var, Vercel cron routes that never check a secret, and Next.js or Express apps that set no security headers in code. It reads the repository, not your running app, and it does not cover everything here: backups, server-side authorization in general, and admin routes are yours to check by hand. Coverage lists what it checks.
The checklist, item by item
1. Database rules: RLS on every table
What goes wrong. Most apps built on Supabase talk to the database from the browser with a public key. If a table has row level security off, or a policy that says using (true), anyone can read, and sometimes change, every row.
Check. In the Supabase SQL editor:
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;Fix. Enable RLS on each table it returns and add an ownership policy, as the Supabase RLS guide shows. Step by step: RLS disabled in public. On Firebase, the equivalent is a rules file that says allow read, write: if true;.
2. Secret keys in frontend code or public env vars
What goes wrong. Any variable starting with NEXT_PUBLIC_, VITE_, REACT_APP_ or EXPO_PUBLIC_ that your frontend reads is built into the bundle every user downloads. A Stripe secret key or a Supabase secret key there is public.
Check. Build, then search what you would ship:
npm run build
grep -rnoE "(sk|rk)_live_[A-Za-z0-9]+|sb_secret_[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]{20,}" dist .next/static build 2>/dev/nullFix. Move the call that needs the key into a server route or Edge Function, then rotate the key, because it has already shipped. More in how to hide an API key in the frontend.
3. Keys in git history
What goes wrong. You committed .env once, then deleted it. The key is still in every clone of the repository, in the commit that added it.
Check.
git log --all --oneline -- .env .env.local .env.production
gitleaks git -vThe first shows whether an env file was ever committed. gitleaks scans the whole history for key formats.
Fix. Rotate every key you find first; rewriting history comes second, because the old commits may already be copied. GitHub's guide to removing sensitive data covers the rewrite. The full walkthrough: accidentally pushed a .env file to GitHub.
4. Authorization on the server, not just hidden buttons
What goes wrong. The admin button only shows for admins, and the order page only links to your own orders, but the API behind them answers anyone who asks. OWASP ranks broken access control first in its Top 10.
Check. Make two accounts. Logged in as A, open the network tab, find a request that carries an id (/api/orders/123), and replay it with an id that belongs to B. Do the same for an admin action while logged in as a normal user. Anything other than a refusal (401, 403 or 404) is a hole.
Fix. Every server route checks who is calling and whether that user owns the record or holds the role. A client-side if (!user) router.push('/login') is a convenience, not a check.
5. Payment webhooks verified
What goes wrong. Your webhook route marks orders paid when it receives checkout.session.completed. Without a signature check, anyone can send that event and get the product free.
Check. Open your webhook handler and look for stripe.webhooks.constructEvent (or your provider's verify call) running on the raw request body before anything else. Code that does JSON.parse(req.body) and trusts event.type is unverified.
Fix. Per Stripe's signature docs, verify with the raw body and the Stripe-Signature header, and stop on failure:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// handle event.type here
res.json({ received: true });
});Register this route before app.use(express.json()). Framework-specific versions are in Stripe webhook signature verification.
6. Packages attackers are already exploiting
What goes wrong. Not every audit warning is equally urgent. CISA's Known Exploited Vulnerabilities catalog lists vulnerabilities that have been exploited in the wild. A package of yours with a CVE on that list is the one to fix tonight.
Check.
npm audit --omit=devFor each Critical and High result, open the advisory, take its CVE id and search for it in the KEV catalog. OSV-Scanner finds known vulnerabilities in more ecosystems than npm; the KEV lookup works the same way.
Fix. npm audit fix, or upgrade the named package to the fixed version. Avoid npm audit fix --force without reading what it changes; per the npm docs, it can install major version updates.
7. AI keys and model output
What goes wrong. Two separate things. An OpenAI or Anthropic client created in the browser with dangerouslyAllowBrowser: true and your own key hands that key to every visitor, and anyone can spend your credits. And model output passed into HTML, SQL or a shell is untrusted input; OWASP's LLM05 lists XSS, SQL injection and remote code execution as the results.
Check.
grep -rnE "dangerouslyAllowBrowser|dangerouslySetInnerHTML|innerHTML|eval\(|exec\(" src app pages lib 2>/dev/nullFor each hit, ask whether model output can reach it.
Fix. Call the model from a server route and keep the key there. Render model output as text, or sanitise it before it becomes HTML. Never run model-written SQL or commands directly. Set a monthly spend limit in your provider's dashboard.
8. Admin and debug routes exposed
What goes wrong. Routes like /admin, /api/seed or /api/debug, built while you were testing, still answer in production unless something stops them.
Check.
find . \( -path ./node_modules -o -path ./.next -o -path ./.git \) -prune -o -type f \( -path "*/api/*" -o -path "*/admin/*" \) -printOpen each route you don't recognise in a private window, logged out.
Fix. Delete what you don't need. What stays checks an admin role on the server (item 4).
9. Cron endpoints with no secret
What goes wrong. A scheduled job is just a public URL. If /api/cron/send-reminders doesn't check a secret, anyone can run it as often as they like.
Check. Open vercel.json, find the crons list, and read each route's handler for a secret check.
Fix. Vercel sends CRON_SECRET as a bearer token. Its cron docs check it like this, failing closed when the variable is unset:
export function GET(request: Request) {
const cronSecret = process.env.CRON_SECRET;
if (!cronSecret || request.headers.get('authorization') !== `Bearer ${cronSecret}`) {
return new Response('Unauthorized', { status: 401 });
}
// run the job
return Response.json({ ok: true });
}10. Backups and a way to restore
What goes wrong. A bad migration, a deleted table, a script run against production. Without a backup that is the end of the data.
Check. Find your most recent backup and its date. Per Supabase's backup docs, Free plan projects get no automatic backups, Pro projects keep 7 days of daily backups, and database backups do not include files in Storage. Lovable Cloud takes a daily backup and keeps roughly 14 days, per its database docs.
Fix. Use a plan with backups, or export on a schedule:
supabase db dump -f schema.sql
supabase db dump --data-only -f data.sqlThen restore once into a scratch project. A backup you have never restored is a guess.
11. Error messages that leak stack traces
What goes wrong. A 500 response that includes the stack trace tells an attacker your file layout, your libraries and sometimes your queries.
Check.
curl -s -X POST https://yourapp.com/api/anything -H "Content-Type: application/json" -d '{bad json'Look for file paths, at stack frames or SQL in the reply.
Fix. Return a generic message and log the details on the server. In Express, run with NODE_ENV=production; the Express error handling guide notes the default handler leaves the stack trace out in production.
12. Security headers and CSP
What goes wrong. Without headers, an XSS bug is easier to exploit and your pages can be framed by another site.
Check.
curl -sI https://yourapp.com | grep -iE "content-security-policy|strict-transport-security|x-content-type-options|x-frame-options|referrer-policy"Fix. In Next.js, set them in next.config.js with headers():
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
];
},
};In Express, use helmet. Add a Content-Security-Policy last, starting as Content-Security-Policy-Report-Only so you see what it would block before it breaks the app. The OWASP headers cheat sheet explains each one.
How to make vibe coding secure after launch
Is vibe coding secure? After these twelve, your app has had the checks that matter most. Run the list again whenever the assistant adds a table, a route or a package, because each of those can reopen an item you already closed. Here is the short vibe coded app security checklist to keep next to the editor:
- RLS on every table, no
using (true)on private data. - No secret key in the build output.
- No key in git history, or rotated if there was.
- Every server route checks the caller.
- Webhooks verify signatures.
- No dependency on CISA's exploited list.
- AI keys on the server; model output treated as untrusted.
- No stray admin or debug routes.
- Cron routes check a secret.
- A backup you have restored once.
- Errors say nothing about your code.
- Security headers set.