SecurityBlog Post

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.

By Abhay Mittal

8 min read

Updated

The Vibe-Coded App Security Checklist: 25 Things to Check Before You Launch
Is your AI-built app exposed? Get a professional vibe coding audit and ship to production with confidence.

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 25 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. Use the parameters recommended by the OWASP password storage guide; do not store plaintext passwords or use MD5.

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 protected route should verify the caller's session and permissions on the server. Keep intentionally public routes explicit and return only data intended for public access.

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. Using accounts you control in a test environment, change the ID to the other account's record and confirm access is denied with a 403 or 404. For a code example, see how to test IDOR in API routes.

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 queries and authorization policies enforce membership of the requested organisation, using a tenant ID derived from verified server-side context. 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.

Limit repeated failed attempts using both account and source signals, with a shared store if your app has multiple instances. Avoid a permanent account lockout that another person could use to deny access.

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.

Verify the unchanged raw request body with stripe.webhooks.constructEvent(body, sig, secret) and reject invalid signatures. See the Stripe webhook signature guide and our webhook review walkthrough.

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 local state is not enough. Enforce your cancellation and paid-through policy on the server; a scheduled cancellation may retain access until the billing period ends.


Infrastructure

24. Row-Level Security is enabled on your Supabase tables.

For tables exposed through the Supabase Data API, review grants, enable RLS, and define policies for each intended operation. With RLS disabled, any role granted access may read or modify data without row-level restrictions. Test as a signed-out visitor and as two separate users. Follow Supabase's RLS documentation and our RLS review guide.

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.


How to Test the Checklist Before Launch

Use a staging environment, two accounts you control, and your payment provider's test mode. Record the request and response for each check so a working screen is not your only evidence.

TestExpected result
Account B requests account A's private recordAccess denied; no private fields returned
A signed-out visitor requests a protected API routeAuthentication required; no private data returned
A regular user calls an admin API route directlyPermission denied on the server
A webhook arrives without a valid signatureRejected before any subscription or order changes
The same verified payment event arrives twiceOne fulfillment; no duplicate entitlement or order
A browser request changes a price or discountThe server uses its own validated product and pricing data
A user exceeds the configured login or AI request limitFurther attempts are limited without bypassing the policy

For the checkout test, use our server-side price validation example. For a broader review plan, follow how to review vibe-coded apps.

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.

Book an audit call → — discuss your app, choose the right audit scope, and get a fixed-price quote.


*VibeAudits provides professional security and reliability audits for apps built with AI coding tools. Explore our audit scope and deliverables and published client case studies.

Keep reviewing your app

Practical checks for the parts of an AI-built app that handle real users and money.

Need a second pair of eyes? Explore our code audit services, scope and pricing, and client case studies.

VibeAudits

Security Experts

Worried your vibe-coded app has issues like this?

We run professional code audits for SaaS apps and AI features built with Cursor, Claude, Copilot, Lovable and Replit. We find the security and reliability problems before your customers (or attackers) do, then hand you a fix-ready report.