Vibe-coded app security checklist: what to check before you launch
Security checks for an AI-built app, each with a one-line test: keys in the bundle, Supabase without RLS, public .env files, headers, loose CORS.
LLaunchScaler·Published ·7 min read
The security checks that matter most before an AI-built app launches cover secret API keys in the browser bundle, Supabase tables without row-level security, a public .env or .git folder, missing security headers and HTTPS redirect, and CORS that trusts any origin with credentials. Each one has a test a non-security founder can run in about a minute, and each fix is small once you know it's there.
The list is ordered by damage: the first two can hand your whole database to a stranger, the last few make other attacks easier. Run every test against your production URL before launch, and again after any large generated change.
What should you check before launching a vibe-coded app?
Five areas cover the failures most likely to put your users' data at risk. The table gives the one-line test for each and what a pass looks like; the sections below explain each result and the fix.
Check
One-line test
Pass
Secret keys in the client bundle
Build, then grep -rE "sk_live_|rk_live_|sb_secret_" .next/static dist
No matches
Supabase row-level security
select tablename from pg_tables where schemaname = 'public' and not rowsecurity;
No rows
Public , , source maps
Questions, answered
What people ask about this
01
Are vibe-coded apps secure?
They can be, if you check the failures that do the most damage: secret keys shipped in the browser bundle, database tables without row-level security, public .env files, missing security headers and permissive CORS. Each can be tested in a minute before launch.
02
Is it safe to put my Supabase anon key in the frontend?
No Access-Control-Allow-Origin: https://evil.example beside Access-Control-Allow-Credentials: true
Are your API keys in the client bundle?
Anything the browser needs is sent to every visitor, and both major build tools make that explicit with a prefix. Next.js inlines every NEXT_PUBLIC_ variable into the JavaScript bundle at build time, and Vite exposes every VITE_ variable in client code; Vite's docs say these "should not contain sensitive information such as API keys."
The failure in generated code is a secret key given a public prefix so a client component can call an API directly: NEXT_PUBLIC_OPENAI_API_KEY, VITE_STRIPE_SECRET_KEY, NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY. Anyone can open DevTools and copy it.
Build the app (npm run build).
Search the output for secret-key prefixes: grep -rE "sk_live_|rk_live_|sb_secret_" .next/static dist 2>/dev/null covers Stripe's live secret and restricted keys and Supabase's new secret keys. Legacy Supabase service role keys are JWTs with no readable prefix, so also search for the first 20 characters of your actual key, and add the prefixes your other providers document.
List your environment variables and check every one with NEXT_PUBLIC_ or VITE_ against a simple rule: would you paste this value on your homepage?
For any secret that fails, move the call that uses it to server code (a Route Handler, a server action or a serverless function) and read the key there without the prefix.
Rotate the key at the provider. It has already shipped, and Next.js notes that public values are frozen into the build, so an old bundle keeps the old key until it's gone.
Is row-level security enabled on every Supabase table?
It must be, because your Supabase publishable (anon) key is public by design. Supabase's docs say that key is safe to expose online only because "Row Level Security decides what this client can reach," and that a table in an exposed schema without RLS "is readable and writable by any role with a grant on it."
Three ways to check, from quickest to most thorough:
The dashboard: open Advisors, then Security Advisor. Supabase's lint 0013_rls_disabled_in_public is an error-level finding titled "Table publicly accessible," and it says anyone with your project URL can read, edit and delete all data in that table.
SQL: run select tablename from pg_tables where schemaname = 'public' and not rowsecurity; in the SQL editor. PostgreSQL's pg_tables view has a rowsecurity column that is true when row security is enabled, so any row returned is a table with RLS off.
By hand: with only the publishable key, request each table from your app's browser console and confirm you get nothing you shouldn't.
The fix is two statements per table. alter table public.todos enable row level security; closes the table entirely; Supabase says that once RLS is on, no data is accessible through the API with a publishable key until you add policies. Then add a policy per action, for example create policy "Users can view their own todos." on todos for select to authenticated using ((select auth.uid()) = user_id);.
Two related rules from Supabase: the secret (service role) key bypasses every RLS policy, so it must never be in a browser, a shipped app or source control; and Supabase rejects secret keys used from a browser by returning 401. If generated code needs the service role key to work, the logic belongs on the server.
Are your .env, .git and source map files public?
Request them and read the status. curl -s -o /dev/null -w '%{http_code}' https://yoursite.com/.env and the same for /.git/HEAD should both print 403 or 404. A 200 with KEY=value lines or ref: refs/heads/main means your secrets or your repository are public.
If either leaks, rotate every secret the file ever held before anything else. GitHub's guidance for exposed secrets makes revoking or rotating the first step, because removing the file doesn't recall copies already made. The exposed .env and .git guide has the full check and the rotation order.
Source maps are the smaller cousin. Next.js turns production browser source maps off by default "to prevent you leaking your source on the client," unless productionBrowserSourceMaps is set to true. Search your config for that flag, and request one of your JavaScript files' .map URL to confirm it returns 404.
Are your security headers and HTTPS redirect in place?
Request the site over HTTP and HTTPS and read the headers. The HTTP request should return a 301 or 308 to the same host over https://, and the HTTPS response should carry Strict-Transport-Security, Content-Security-Policy, X-Frame-Options (or a CSP frame-ancestors), X-Content-Type-Options: nosniff and Referrer-Policy.
Mozilla's HTTP Observatory, which grades these headers from a starting score of 100, takes 25 points for a missing Content-Security-Policy, 20 each for missing HSTS, missing clickjacking protection and a missing HTTPS redirect, and 5 for missing nosniff. The security headers checklist gives the value to send for each and how to set them on Next.js, Vercel, Netlify and Nginx. The CSP is the one that needs care, because a wrong one breaks the app; the Content-Security-Policy starter guide rolls one out in report-only mode first.
Does your API trust any origin with credentials?
It shouldn't. The dangerous CORS pattern is an API that copies whatever Origin header it receives into Access-Control-Allow-Origin and also sends Access-Control-Allow-Credentials: true. Then any website a signed-in user visits can call your API with their cookies and read the response.
Browsers refuse the lazy version: MDN documents that credentials are not supported when Access-Control-Allow-Origin is *, and warns that sending credentials cross-origin can make a site vulnerable to cross-site request forgery. Reflecting the origin gets around that block, which makes it a tempting shortcut when a CORS error appears during development. The Observatory takes 50 points for content visible cross-origin with credentials allowed.
Test it with an origin you don't own: curl -sI -H "Origin: https://evil.example" https://api.yoursite.com/me | grep -i access-control. If https://evil.example comes back beside Access-Control-Allow-Credentials: true, replace the reflection with an explicit allowlist of your own origins, the approach OWASP's cheat sheet shows (Access-Control-Allow-Origin: https://yoursite.com). If the API doesn't need cookies cross-origin, drop the credentials header; MDN says to omit it rather than set it to false.
What can't these tests catch?
Anything that depends on who is signed in. None of the checks above can tell whether user A can load user B's invoice by changing an ID in the URL, whether an admin route checks the role, or whether a coupon can be applied twice. ZAP's own docs say automated scanning won't detect logical flaws such as broken access control.
Test those with two accounts: sign in as A, copy a request that loads one of A's records, change the ID to one of B's, and send it. Anything of B's that comes back is a bug to fix before launch. Do the same for every route that takes an ID, and for every write, not only reads. Whether the app is also findable is a separate launch check; the AI-built app indexing guide covers it.
Get the evidence and the fix for each finding
LaunchScaler's free scan runs the outside-in part of this checklist on your live site, with no account: its 29 security checks read .env and .git exposure, published source maps, security headers, HSTS, the HTTPS redirect, TLS, cookie flags, wildcard CORS, SPF, DKIM and DMARC, alongside 127 other checks across 6 of its 7 categories. It can't test your RLS policies or access control, which need the manual tests above. The full audit, $19 once for the domain, opens every finding to its evidence (the exact response your site sent) and the exact fix, and adds 40 more checks. Run the free scan first, then open the full audit from your report.
Yes, if row-level security is enabled on every table. Supabase says the publishable (anon) key is safe to expose because it only reaches what RLS allows. The secret (service role) key bypasses RLS and must never reach the browser.
03
Are NEXT_PUBLIC_ and VITE_ environment variables secret?
No. Next.js inlines NEXT_PUBLIC_ values into the JavaScript sent to the browser, and Vite exposes VITE_ variables in client code. Anything with those prefixes is visible to every visitor.
04
How do I check if my vibe-coded app leaks secrets?
Build the app and search the output for secret-key prefixes such as Stripe's sk_live_ or Supabase's sb_secret_, then run curl -s -o /dev/null -w '%{http_code}' https://yoursite.com/.env and expect a 403 or 404. Rotate any key you find exposed.
05
What can't an automated security scan check?
Anything that depends on who is signed in: whether user A can read user B's data, whether your database policies are correct, and business rules such as coupons or trials. Test those by hand with two accounts.
AVIF is usually smaller than WebP at the same quality but encodes more slowly. All major browsers now support both, so serve AVIF with a WebP fallback.