# Vibe Coding Security Checklist: 12 Checks Before You Launch

URL: https://tuvio.dev/blog/vibe-coding-security-checklist
Published: 2026-10-19
By the Tuvio team

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](https://tuvio.dev/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](https://tuvio.dev/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](https://supabase.com/docs/guides/database/postgres/row-level-security) shows. Step by step: [RLS disabled in public](https://tuvio.dev/blog/supabase-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](https://tuvio.dev/blog/hide-api-key-in-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](https://github.com/gitleaks/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](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository) covers the rewrite. The full walkthrough: [accidentally pushed a .env file to GitHub](https://tuvio.dev/blog/accidentally-pushed-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](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/) 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](https://docs.stripe.com/webhooks/signature), 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](https://tuvio.dev/blog/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](https://www.cisa.gov/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](https://google.github.io/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](https://docs.npmjs.com/cli/commands/npm-audit), 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](https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/) 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](https://vercel.com/docs/cron-jobs/manage-cron-jobs) 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](https://supabase.com/docs/guides/platform/backups), 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](https://docs.lovable.dev/features/database).

**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](https://expressjs.com/en/guide/error-handling.html) 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()`](https://nextjs.org/docs/app/api-reference/config/next-config-js/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](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html) 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.
