Vibe Coding Security Checklist

  • Aug 26

Vibe Coding Security Checklist: The 9 Checks I Run Before Real Users Touch an App

Nine security checks I run before real users touch an AI-built app, each with a way to prove it actually passed, plus the cost risk most lists skip.

Nobody gets breached the way the checklists imagine. In the apps I actually ship, the first thing to go wrong is never a clever injection chain. It is a key that made it into the browser bundle, a table that turned out to be readable by anyone who found the project URL, or an endpoint that costs me real money per call and had no ceiling on it.

I run about fourteen products on Supabase and Vercel. Most of them were built fast, with an agent doing the typing. That is what vibe coding is now: you describe the app, the model writes it, you ship in days instead of quarters. The code usually works. What gets you is the part nobody described, because you cannot describe a threat you have not thought about yet.

The security lists you find for this are mostly written by security vendors, and they are not wrong. They are just written by people who have never had to explain a four hundred dollar model bill to themselves at 2am. They give you the item and stop. Enable row level security. Sanitise input. Rotate secrets. Almost none of them tell you how to prove the item is actually done, which is the only part that matters, because an agent will cheerfully tell you it enabled something it did not enable.

So here is the list I run before real users touch anything. Nine checks, ordered by what actually breaks first, each with a way to verify it from outside your own code. On a small app it takes me about forty minutes.

What actually breaks first in an AI-built app

Three things go wrong, and they go wrong in this order.

Exposure. Something that should have stayed on the server is now in a file the browser downloads. This is the most common one by a wide margin, and it is almost always an environment variable with the wrong prefix.

Authority. The app checks who you are in the interface but not in the database, so anyone who can make an HTTP request can act as anyone else. Agents are very good at building the interface check and very bad at remembering the database one.

Cost. The app has a public route that calls a metered API, and there is no ceiling on it. Nobody has to be malicious for this to hurt. A loop in someone's test script does it. A post that travels further than you expected does it.

Standard security advice covers the first two properly and treats the third as a footnote about denial of service. For a solo builder paying per token, per image, and per row, the third is the one that empties an account overnight. It goes on the list as a first-class item.

The vibe coding security checklist

1. Find every secret that reached the browser

Frameworks decide what ships to the client by naming convention, not by intent. Anything prefixed with NEXT_PUBLIC is compiled into the bundle and served to every visitor. An agent that needed a value on the client and hit a wall will sometimes just add that prefix and move on. The build succeeds. Nothing warns you.

How to verify: build the app, then search the built output directory for the first eight characters of each secret you own. Not the variable name, the value. If a secret appears in a built file, it is public, and it is public in your deploy history too. Do not fix it by removing the prefix. Rotate the key first, then remove the prefix, because the old value is already out.

Do the same search on your git history. A key that lived in a committed environment file for one afternoon in March is still in the repository forever.

2. Turn on row level security, then prove it from outside

Row level security is the single control that decides whether your database is a database or a public API. Supabase is explicit about this in its row level security documentation: a table in an exposed schema without it is readable and writable by any role holding a grant. There is no soft failure mode. It is either on with a policy, or it is open.

The catch is that turning it on and writing a policy are two separate acts, and an agent frequently does only one of them. A table with row level security enabled and zero policies denies everything, which looks like a bug, and gets "fixed" by adding a policy that allows all. I have watched that exact sequence happen inside a single session.

How to verify: stop reading your own code. Open a private browser window, take the public anon key from your own front end, and query each table directly with it while signed out. Then sign in as a second test user and try to read the first user's rows. Anything that comes back that should not is a finding. This is a five minute test and it is the highest value five minutes on the whole list.

3. Treat the service role key as a loaded gun

The service role key bypasses row level security entirely. That is its job. Supabase says plainly that it should never be used in the browser or exposed to customers, and yet it turns up in client code constantly, because when an agent hits a permissions error the fastest way past it is the key that has no permissions problems.

How to verify: search the whole repository for the service role key and confirm every hit is in a server route, an edge function, or a scheduled job. Then check the reverse: any server route holding that key has to do its own authorisation, because the database will no longer do it for you. A route that takes a user ID from the request body and trusts it, while holding a key that bypasses every policy, is an account takeover with extra steps.

I went through the agent side of this in the rules I run before an agent touches any of my projects, which covers scoping the access itself rather than the app that uses it.

4. Put a spending ceiling on every route that costs money

This is the check missing from every list I have seen, and it is the one that has actually cost me money.

If your app has a route that calls an image model, a language model, an enrichment API, or anything else billed per request, and that route is reachable without a session, you have handed the internet a button that spends your money. The damage does not require an attack. It requires attention.

How to verify: list every outbound paid call your app makes. For each one, answer three questions. Can it be triggered without signing in? Is there a per-user cap on how many times a day it can run? Is there a hard spend alert on the vendor account that fires before the number hurts?

If any answer is no, fix it before launch. The cheap version is a credit counter in your own database, decremented in the same transaction that queues the work, plus a budget alert at the vendor. The expensive version is finding out from a billing email.

Charging for the thing is not a substitute for capping it, either. Signed-in users can loop too, and a generous free tier is the same open button with a login form in front of it.

5. Rate limit by user, not just by address

Address-based rate limiting is what you get by default and it is close to useless on its own. Mobile networks put thousands of people behind one address, so a limit tight enough to matter locks out real users, and anyone determined has a proxy pool.

How to verify: hit your own most expensive route fifty times in a loop from a signed-in session and see what happens. If all fifty succeed, you have no limit, only the belief that you have one. Limit on the account, the session, and the address, in that priority order, and return a clear error rather than failing silently.

6. Verify webhook signatures before you trust a payment event

A payment webhook endpoint is a public URL that grants access when it likes what it reads. If it does not verify the signature, anyone who guesses the path can post a fake success event and upgrade themselves for free. Stripe is blunt about this in the webhook documentation: without verification, an attacker can trigger fulfilment, account access, or record changes at will.

How to verify: post a hand-made JSON body to your own webhook route with no signature header and confirm you get a rejection, not a 200. Then confirm your framework is not mangling the raw request body before verification runs, which is the usual reason a correct implementation still fails. And log the event IDs you have already processed, because the same event will arrive twice eventually, and a duplicate upgrade is its own kind of bug.

If you are running more than one product through one payment account, the blast radius question matters too, which I worked through in one account or ten, and what that choice costs later.

7. Check what your uploads accept and where they land

Agent-written upload handlers validate the file extension in the browser and stop there. The extension is a suggestion. The content type header is a suggestion. Neither one is a control.

How to verify: rename an HTML file so it ends in an image extension and upload it. If it stores, and if the storage bucket serves it back at a public URL with its original content type, you have just hosted an attacker's page on your own domain. Cap the file size at the server, check the actual bytes rather than the name, and confirm the bucket is private unless you have a specific reason for it not to be.

8. Make your errors boring

Debug output is enormously useful during a build and is an information gift after launch. Stack traces name your framework version, your file paths, sometimes your table names, and occasionally the query that failed with a value still inside it.

How to verify: deliberately break something in production. Post malformed JSON to an API route. Request a record that does not exist. Read exactly what comes back to the browser. It should be a short message and a status code, with the detail sitting in your server logs where only you can see it.

9. Decide who can delete, and write it down

Every app I have built has a moment where a destructive action ends up with weaker protection than a read. Deletes get built last, usually in a rush, often with the service role key because the policy work was annoying.

How to verify: for every destructive route, confirm the authorisation check names the owner of the record rather than merely requiring a signed-in user. Then confirm you have a backup you have actually restored from once, and a row in an audit table for every delete. Point-in-time recovery you have never tested is a hope, not a backup.

How to run this checklist with the agent that built the app

The agent that wrote the code is the fastest way to audit it, as long as you refuse to let it grade itself. Ask for evidence, not reassurance. This is roughly what I paste:

Audit this repository against the following list. For every item, answer in three parts: the file and line where the control exists, the exact command or request I can run myself to confirm it works, and what a failure would look like. If a control does not exist, say it does not exist. Do not tell me a control is fine because it is standard practice, and do not fix anything yet. The items: secrets exposed to the client bundle, row level security enabled with a real policy on every table, service role key confined to server code, spending caps on every paid outbound call, per-user rate limits, webhook signature verification, upload validation at the server, error output in production, and authorisation on destructive routes.

The "do not fix anything yet" line matters more than it looks. An agent given permission to fix will produce a large diff that touches auth, and you will end up reviewing a rewrite instead of a report. Get the report, read it, then fix the items one at a time.

The other half is remembering that a finding is not fixed until you have re-run the verification yourself. I have been told a table was locked down by a model that had, in fact, enabled row level security and written no policy at all.

The three places secrets live, and why they drift

On this stack a secret can live in three separate stores, and they do not talk to each other. Edge function secrets sit in the Supabase dashboard. Deployment environment variables sit in the Vercel dashboard. Your local file sits on your laptop. A key rotated in one place and not the others produces a failure that only shows up in one environment, usually the live one, usually on a weekend.

Two habits fix most of it. Write down which store owns which key, in the project instruction file, so the next session does not have to guess. And before you rotate anything, check what is encrypted with it, because rotating a key that encrypts stored user credentials does not refresh them, it destroys them.

The wider version of this problem, everything that exists in your working environment and does not travel with your code to production, is the subject of the six things that do not travel with your code.

What I skip on a pre-revenue app

Honest admission, because a checklist you cannot finish gets abandoned entirely.

On an app with no users and no revenue, I do not run a penetration test, I do not stand up a web application firewall, I do not do compliance work, and I do not write an incident response plan. Those are real controls and they matter at real scale. Running them on a weekend project is how people spend three weeks on security and never launch.

What I do not skip, ever, is the nine above, because eight of them take minutes and the ninth is the difference between a bad week and a lost database. The line I draw is simple. Anything that protects user data or my own bill is not optional at any size. Anything that mainly protects against a targeted attacker can wait until someone would bother targeting me.

Where this came from

Most of what is on this list I learned by shipping the mistake first. I went through the full build-and-deploy path, including the parts that only bite once an app is public, in Turn OpenClaw or Claude Code into Full App Builders. That session has already run, but the full replay is available instantly, so if you would rather watch the whole build than read a summary of it, it is there now.

If you want the ongoing version, the Reinventing AI Accelerator is where I run the live sessions each week and where the vault of cloneable apps, tools, and agents lives. Members get the working builds, not just the write-ups, which means you can read these security decisions inside code that is already running rather than reconstructing them from a list. If that is useful, come in. If you would rather just take the nine checks and run them on your own app this afternoon, that is a completely fine outcome for this post.

Frequently asked questions

Is vibe coding safe enough for a real product?

Yes, with the caveat that the safety comes from what you verify after the build rather than from the build itself. Generated code is not systematically worse than hand-written code on these nine items. It is worse on one specific axis: it produces a confident, complete-looking result whether or not the control is real, so the usual instinct that unfinished work looks unfinished does not protect you. Verify from outside the code and that gap closes.

What is the most common security mistake in AI-generated apps?

A secret with a public prefix ending up in the browser bundle, followed closely by row level security enabled with no policy behind it. Both come from the same root cause: the agent hit a permissions error, took the fastest route past it, and the build succeeded, so nothing signalled that anything was wrong.

Can I just ask the AI to check its own code for security issues?

You can, and it is a good first pass, but only if you demand file references and a command you can run yourself for every claim. Asked plainly, a model returns a well-formatted list of controls that sounds authoritative and is partly aspirational. Asked for evidence, it is genuinely useful. The prompt above is written that way on purpose.

Do I need a penetration test before launch?

Not for a small app with no revenue and no sensitive data. Spend that time and money on the nine items instead. Once you are holding customer records, payment relationships, or anything regulated, the answer changes, and it changes before you feel ready for it to.

How do I know if my API keys are already exposed?

Search your built output and your git history for the first several characters of each key value rather than for the variable name. If you find one, treat it as burned: rotate the key at the vendor first, then clean the code. Cleaning the code without rotating leaves a working key sitting in a public history.

Subscribe Now for More AI Insights

Subscribe for Updates from Reinventing AI

Stay current on the most cutting-edge AI solutions for ambitious entrepreneurs and marketers!

Get weekly AI training announcements, AI resources and insights.

0 comments

Joinor login to leave a comment