Scan free

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

#ItemWhy it mattersHow to check
1Database rules (RLS)Anyone can read every user's rowsOne SQL query
2Secret keys in frontend codeKeys ship to every visitorGrep the build output
3Keys in git historyDeleted is not gonegit log or gitleaks
4Server-side authorizationHidden buttons stop no oneReplay a request with another id
5Payment webhooks verifiedFake "paid" eventsGrep for the signature check
6Exploited packagesKnown attacks, already in usenpm audit against CISA's list
7AI keys and model outputBills, XSS, injectionGrep for browser clients and raw HTML
8Admin and debug routesFree admin panelList your routes, open them logged out
9Cron endpointsAnyone can run the jobRead each cron handler
10Backups and restoreOne bad migration ends itFind your last backup
11Error messagesStack traces map your codeSend a broken request
12Security headers and CSPEasier XSS and clickjackingcurl -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:

sql
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:

bash
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/null

Fix. 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.

bash
git log --all --oneline -- .env .env.local .env.production
gitleaks git -v

The 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:

javascript
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.

bash
npm audit --omit=dev

For 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.

bash
grep -rnE "dangerouslyAllowBrowser|dangerouslySetInnerHTML|innerHTML|eval\(|exec\(" src app pages lib 2>/dev/null

For 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.

bash
find . \( -path ./node_modules -o -path ./.next -o -path ./.git \) -prune -o -type f \( -path "*/api/*" -o -path "*/admin/*" \) -print

Open 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:

typescript
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:

bash
supabase db dump -f schema.sql
supabase db dump --data-only -f data.sql

Then 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.

bash
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.

bash
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():

javascript
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:

  1. RLS on every table, no using (true) on private data.
  2. No secret key in the build output.
  3. No key in git history, or rotated if there was.
  4. Every server route checks the caller.
  5. Webhooks verify signatures.
  6. No dependency on CISA's exploited list.
  7. AI keys on the server; model output treated as untrusted.
  8. No stray admin or debug routes.
  9. Cron routes check a secret.
  10. A backup you have restored once.
  11. Errors say nothing about your code.
  12. Security headers set.