The vibe coding security checklist: 12 things to check before you share your app
A vibe-coded app is as secure as the prompts that built it, and most prompts never mention security. These 12 checks cover what actually goes wrong in apps built with Lovable, Replit, Bolt, v0 and Cursor. Each one takes about a minute to check and comes with a prompt that fixes it.
How secure is vibe coding, really?
Vibe coding is not insecure by nature, but it is insecure by default. AI builders optimize for the app working, and an app can work perfectly while its OpenAI key sits in the JavaScript every visitor downloads or its database accepts reads from anyone with the public key. Security has to be asked for, and most people do not know what to ask.
The good news is that the list of things that go wrong is short and repetitive. The same eight or nine mistakes show up across every builder, because every builder produces roughly the same stack: a React front end, a hosted database (Supabase or Firebase), and a few calls to an AI provider. Fix those and you are ahead of most apps on the internet, vibe-coded or not.
Work through the list in order. The first three are the ones that cost real money when missed.
1. No secret key has ever been in front-end code
Anything in your app's JavaScript is public. Every visitor's browser downloads it, and anyone can open the developer tools and read it. A secret key in there (OpenAI, Anthropic, Stripe, Resend, your database's admin key) belongs to whoever finds it first.
Check: open your live app, press F12, go to the Network tab, reload, and click the biggest .js file. Search it for sk-, sk_live, re_, service_role and the word secret. Then do the same in your builder's code view for files under src/.
If you find one: rotate it first, in the provider's dashboard. Deleting a key from the code does not un-leak it. Then move the call that used it to a server function (see check 3). The full procedure is in what to do when an API key is in your front-end JavaScript.
2. Nothing secret lives in a VITE_, NEXT_PUBLIC_ or REACT_APP_ variable
Build tools copy every environment variable with one of these prefixes straight into the browser bundle. That is what the prefix means: public. A variable named VITE_OPENAI_API_KEY is exactly as secret as a billboard. Lovable and Bolt apps use Vite, v0 apps use Next.js, and Replit Agent apps often use Vite too, so this applies to nearly everyone.
Check: list your environment variables in the builder (Lovable: project settings; Replit: the Secrets pane; v0 and Vercel: project environment variables). Any value that starts with one of those prefixes and holds a secret is leaking.
Fine to be public: the Supabase URL and anon key, the Firebase web config, the Stripe publishable key (pk_), and analytics IDs. Those are designed to ship in the browser, as long as the database rules behind them are right (checks 4 to 6).
3. AI calls go through a server, never straight from the browser
If your app talks to OpenAI or Anthropic directly from the browser, it is sending your key to every visitor. The SDKs make you turn on a setting literally named dangerouslyAllowBrowser to allow it, and AI builders will happily turn it on when the first attempt fails.
Check: search your code for dangerouslyAllowBrowser and for api.openai.com or api.anthropic.com inside anything under src/. Either one means the key is in the browser.
Fix: move the call to the server side your builder gives you: a Supabase Edge Function (Lovable, Bolt), an Express route (Replit), a Next.js Route Handler (v0), or an API route in whatever Cursor set up. The key then lives in that function's secrets and the browser calls your function instead.
Security fix: my app calls the OpenAI API from the browser with my API key, so anyone visiting can copy the key.
Move the OpenAI call into a server-side function, read the key from a server-only secret named OPENAI_API_KEY, and have the front end call that function instead.
Remove the key and dangerouslyAllowBrowser from every front-end file and from any VITE_ or NEXT_PUBLIC_ variable. Do not change the UI.
4. Every Supabase table has Row Level Security on
Your Supabase anon key is public by design. Row Level Security (RLS) is the only thing that stops someone with that key from reading your whole users table with a single request. A table created without RLS is a public table, whatever your app's login screen suggests.
Check: in the Supabase dashboard open the SQL editor and run:
select tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Every row that comes back is a table anyone can read and write. The full walk-through, including the policies to add, is in Supabase RLS in Lovable apps. It applies to any builder that uses Supabase, including Lovable Cloud.
5. No policy says using (true)
RLS on with a policy that lets everyone in is RLS off with extra steps. AI builders write using (true) when a feature breaks and they want it to work again. It means "this rule passes for everybody".
Check: in the same SQL editor:
select tablename, policyname, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true');
A true on a SELECT policy for a genuinely public table (a blog, a product list) is fine. On INSERT, UPDATE or DELETE, or on any table with personal data, it is a hole.
6. Firebase rules are not in test mode
Firebase projects start in "test mode", where the rules allow every read and write for 30 days and then shut everything off. People either ship in test mode, or hit the shut-off and ask their builder to "fix the permission error", which produces rules like allow read, write: if true;.
Check: open firestore.rules, storage.rules or database.rules.json in your project. Any if true, or a rule that only checks request.auth != null on data that belongs to specific users, needs tightening.
7. The Supabase service_role key is nowhere near the browser
The service_role key ignores RLS entirely. It is for servers only. In front-end code it means every row in every table is readable, editable and deletable by anyone who looks. This is the single worst finding a scan can turn up, and it happens because a builder reached for it to make an admin page work.
Check: search the whole project for service_role and for keys starting sb_secret_. They may only appear in server functions and their secrets.
8. No .env file is committed to GitHub
Lovable, Replit and Bolt sync your project to GitHub, and Cursor commits whatever you tell it to. If a .env file is in the project and not in .gitignore, it is in the repo, in every clone, and in the history forever.
Check: open the repo on GitHub and look for .env, .env.local or .env.production at the top level. Then check .gitignore contains .env*.
If it is there: rotate every key inside it. Removing the file in a new commit leaves the old commit readable. Rotation is the fix; the removal is housekeeping.
9. Known-vulnerable packages are updated
Your app runs on a few hundred open source packages, and some of the versions your builder picked have published vulnerabilities. Most are not emergencies. A few (anything in your auth, file upload or templating path) are.
Check: on GitHub, the Security tab shows Dependabot alerts if they are enabled. Locally, npm audit in the project folder lists them. Tell your builder to update the named packages.
10. Admin checks happen on the server
If the question "is this user an admin?" is answered from localStorage, a React state variable or a field the browser sends, anyone can answer yes by editing it in the developer tools. The UI hiding a button is not security; the server refusing the request is.
Check: search for isAdmin, role and localStorage in src/. Wherever an admin decision is made, there must be a matching check on the server or in an RLS policy that the browser cannot skip.
11. Stripe webhooks verify their signature
A webhook handler that marks orders as paid without checking the request actually came from Stripe can be called by anyone, with any payload. The check is one line using your webhook signing secret, and builders skip it because the first test works without it.
Check: find the webhook route and look for constructEvent (Node) or the equivalent signature check. If the handler reads req.body as JSON and acts on it directly, it is not verifying.
12. The basics of how the site is served
Four small settings, all one prompt away: plain http:// redirects to https://; an HSTS header tells browsers to always use HTTPS; X-Frame-Options or a frame-ancestors rule stops other sites loading yours in an invisible frame; and a Content Security Policy limits which scripts can run. None is an emergency on its own. Together they are what separates a site that looks professional to a security review from one that does not.
Check: any free header checker, or your own developer tools under Network, Headers. Platform addresses (lovable.app, replit.app) handle HTTPS for you. On a custom domain, check it yourself.
The whole list on one screen
| Check | Where it hides | If missed |
|---|---|---|
| 1. No secret in front-end code | the built JavaScript | someone spends your API budget |
| 2. No secret in VITE_ / NEXT_PUBLIC_ vars | environment settings | same as 1, with extra confidence |
| 3. AI calls go through a server | dangerouslyAllowBrowser | your key ships to every visitor |
| 4. RLS on every Supabase table | pg_tables.rowsecurity | anyone reads your users table |
| 5. No using (true) policies | pg_policies | anyone edits or deletes rows |
| 6. Firebase rules not in test mode | firestore.rules, storage.rules | anyone reads or writes your data |
| 7. service_role key never in the browser | src/, VITE_ vars | total loss of the database |
| 8. No .env in the repo | GitHub, .gitignore | every key in it is public |
| 9. Vulnerable packages updated | package-lock.json | a known hole, with a public write-up |
| 10. Admin checks on the server | localStorage, isAdmin | anyone is an admin |
| 11. Stripe webhooks verified | the webhook route | free orders |
| 12. HTTPS, HSTS, framing, CSP | response headers | a weak first impression and real, smaller risks |
Questions people ask
What is the single most dangerous mistake in a vibe-coded app?
The Supabase service_role key, or any database admin credential, in front-end code. It bypasses every access rule, so one copy of it means every row in the database can be read, changed or deleted by anyone. Leaked AI provider keys are a close second because they cost money within hours.
Do I need to know how to code to fix these?
No. Every check has a prompt you can paste into the builder that made your app. The builder knows what RLS, Edge Functions and CORS are. Your job is to ask for the fix and then confirm it worked, either by re-running the check or by rescanning.
Is a security checklist enough, or do I need a security audit?
For a side project or an early product, this checklist covers the mistakes that actually get exploited. If you handle payments, health data or anything regulated, treat the list as the baseline and get a professional review on top.
How often should I re-check?
After every significant prompt. AI builders re-create these problems when they add features, so the useful habit is to check before each time you share the app, or to let a scanner watch the repo and tell you.