The Vibe-Coded App Security Checklist: 25 Things to Check Before You Launch
A practical, copy-paste security checklist for founders who built their app with Cursor, Lovable, Bolt, or Claude and want to know what to check before going live with real users and real payments.

AI coding tools are genuinely impressive. Cursor, Lovable, Bolt, and Claude can take a non-technical founder from idea to working app in days. That speed is real.
What's also real: AI tools optimise for code that runs, not code that's secure. The things that make your demo work — logins, forms, payments, file uploads — are exactly the things most likely to have serious security problems that won't show up until something goes wrong in production.
This checklist covers the 25 most important security checks for a vibe-coded app. Work through it before you launch, before you take your first real payment, and before you handle real user data.
Secrets & Credentials
1. No API keys, tokens, or passwords hardcoded in the codebase.
Search your repo for common patterns: sk-, pk_, API_KEY, SECRET, PASSWORD, DATABASE_URL. These should live in environment variables, never in source files. Check your git history too — a secret committed and later removed is still in the history.
2. No secrets in client-side code.
Anything prefixed NEXT_PUBLIC_ in a Next.js app is bundled and sent to the browser. Your Stripe secret key, database password, and OpenAI API key should never have that prefix.
3. Environment variables are set in all environments.
Dev, staging, and production each need their own env vars. A missing variable that crashes silently in production is a common launch-day failure.
4. A .gitignore that actually ignores .env files.
Check that .env, .env.local, .env.production, and similar files are in your .gitignore and not tracked in git.
Authentication
5. Passwords are hashed with a modern algorithm.
If your app handles passwords directly (vs. delegating to Clerk or Auth.js), they must be hashed with bcrypt, argon2, or scrypt. Plain text or MD5 passwords are a critical vulnerability.
6. Sessions have an expiry.
Check that your session tokens or JWTs expire. An access token that never expires means a leaked token is a permanent backdoor.
7. JWT secrets are strong and secret.
If you're using JWTs, the signing secret should be a long random string (32+ characters), stored in an environment variable. Not "secret", not "your-jwt-secret".
8. Password reset tokens expire and can only be used once.
A password reset link should stop working after it's used and after some time limit (30–60 minutes). Reusable or non-expiring reset tokens are exploitable.
9. Email verification is actually required before access.
If your app sends a verification email, check that unverified accounts are actually blocked from accessing sensitive features. AI tools sometimes generate the verification email flow without enforcing it.
10. OAuth state parameter is validated.
If you're using Google, GitHub, or another OAuth provider, the state parameter should be generated server-side, stored in a session, and validated on callback. Missing state validation is a CSRF vulnerability.
Authorization
11. Every API route checks who the caller is.
Work through your API routes (or server actions) one by one. Each one that returns or modifies data should verify the caller is authenticated before doing anything.
12. Users can only access their own data.
For any endpoint that fetches by ID (/api/invoices/123, /api/users/456/profile), verify that the ID belongs to the authenticated user. Change the ID to another user's and confirm it returns a 403, not their data.
13. Admin routes are actually restricted to admins.
If your app has an admin panel or admin API routes, test that a regular user account genuinely cannot reach them.
14. Multi-tenant boundaries are enforced at the database level.
If your app serves multiple organisations, check that every database query filters by organisation_id (or equivalent). A query that returns all records and filters client-side is a bug waiting to happen.
Input Validation
15. Database queries use parameterized inputs, not string concatenation.
Search for patterns like `SELECT * FROM users WHERE email = '${email}'`. Any query built by string interpolation is vulnerable to SQL injection. Use parameterized queries or an ORM.
16. File uploads are restricted by type and size.
If your app accepts file uploads, check that it enforces a whitelist of allowed MIME types and a maximum file size server-side. Client-side validation is not enough.
17. User input is sanitized before being rendered as HTML.
If your app renders user-generated content (comments, descriptions, profile bios), make sure it's escaped or sanitized before output. Raw dangerouslySetInnerHTML with user input is an XSS vulnerability.
Rate Limiting
18. Your login endpoint has rate limiting.
Unlimited login attempts enable brute force attacks. Your /api/auth/signin (or equivalent) should reject repeated failed attempts from the same IP.
19. Your password reset endpoint has rate limiting.
Same principle. An endpoint that sends unlimited password reset emails is trivially abusable.
20. AI-powered endpoints have spend limits.
If your app calls OpenAI, Anthropic, or another LLM provider per user request, there should be a per-user or per-session limit. Unlimited LLM calls from a single account can run up large bills fast.
Payments & Webhooks
21. Stripe webhooks verify the signature.
Never trust a Stripe webhook payload without validating stripe.webhooks.constructEvent(body, sig, secret). Without signature verification, anyone can send a fake payment confirmation to your endpoint.
22. Fulfilled orders are idempotent.
If a webhook fires twice (which Stripe will do on retries), your handler should process it exactly once. Check for a unique event ID being stored and skipped on re-delivery.
23. Subscription status is checked server-side before granting access.
Your app should check the user's subscription status on the server for every gated feature. A client-side check that reads from local state is not enough — a paying customer who cancels should lose access within one request, not one reload.
Infrastructure
24. Row-Level Security is enabled on your Supabase tables.
If you're using Supabase with the client SDK (not a service role key), check that RLS is enabled on every table. A table with RLS disabled is readable by any authenticated user in your project.
25. Error messages don't leak implementation details.
Check that your 500 error responses don't return stack traces, SQL query fragments, or internal paths. Those details help attackers understand your system. Log them server-side; return a generic error to the client.
What to Do With This List
Work through it yourself first. Many of these checks take five minutes and a code search. Fix what you find.
Then get a second pair of eyes. The things hardest to catch yourself are the ones missing from the code — an authorization check that was never written, a database query that returns too much, an endpoint that was meant to be protected but isn't. A professional code audit covers all of this and tells you exactly what to fix.
Get a free code assessment → — a 24-hour, no-commitment review of the most critical issues in your app.
VibeAudits provides professional security and reliability audits for apps built with AI coding tools. We've audited over 50 vibe-coded apps and written this checklist based on what we find most often.