Pre-launch · 11 min · Updated 28 Aug 2026
The vibe coding security checklist: 12 checks before you launch
Twelve checks to run on an AI-built app before real customers touch it, in the order that finds the worst things first.
A coding agent writes code that runs. Running is not the same as holding. The gap between the two is narrow and specific: an agent optimises for the feature working when you use it, and almost never for what happens when someone who is not you sends a request you did not anticipate.
So this checklist is ordered by what actually turns up, worst first. Checks 01 to 04 are the ones that end companies. Checks 05 to 09 are the ones that leak. Checks 10 to 12 are hygiene you want in writing before a customer's security questionnaire arrives.
Four of these twelve checks cannot be answered from outside your app, because they need two logged-in accounts. That is not a gap in the checklist, it is the shape of the problem: authorization is invisible from the outside. Where that applies, we say so, and we say what the honest answer is when you skip it.
The four that end companies
Each of these lets a stranger, or any signed-up user, reach data that is not theirs. There is no severity argument to have about them. If one is true, you are not ready to launch.
- 01One customer cannot read another's rows. Sign in as account A, then request a record you know belongs to account B by its ID. The API must refuse. Needs two accounts — no external check can answer this.
- 02Your database is not readable with the key in your bundle. Supabase and Firebase are designed to be queried directly from the browser with a public key. Row Level Security or security rules are the only thing scoping that query to the right rows. Off by default on Supabase tables; permissive by default in Firebase's test mode.
- 03No admin or service key is in the client. A Supabase
service_rolekey, a Firebase admin credential or a database connection string in your JavaScript bypasses every rule you wrote. Anyone who reads your bundle owns your data. - 04Every route that changes data checks who is asking. Agents commonly protect the page and forget the endpoint behind it. A hidden button is not an access control. The check has to be in the handler, on the server, on every method.
# signed in as account A
GET /api/invoices/1041 -> 200 OK { "customer": "A", ... } (mine)
GET /api/invoices/1042 -> 200 OK { "customer": "B", ... } <-- not mine
# what it should return
GET /api/invoices/1042 -> 403 ForbiddenThe five that leak
These do not hand over your database, but they hand over something you meant to keep: a key, a customer file, an internal path, the contents of a config file. Most are visible from outside, which means they are also visible to anyone scanning the internet.
- 01No secret is in the client bundle. Any variable prefixed
NEXT_PUBLIC_,VITE_orREACT_APP_is compiled into the JavaScript the browser downloads. Read the list and ask, for each one, whether you would post it publicly. - 02No
.envor.gitis served. Request/.envand/.git/configon your live domain. Both should 404. A served/.gitdirectory can reconstruct your whole source history, including the secrets you deleted later. - 03Uploaded customer files are not publicly listable. If a file URL works when signed out, it works for everyone. Check whether the bucket also lists its contents — a guessable URL and a listable bucket are different problems with the same cause.
- 04Source maps are not published in production. They hand over your original source, comments included. Useful in staging, an unnecessary gift in production.
- 05No debug, admin or docs route is open. Try
/admin,/debug,/api/docs,/graphqland/openapi.jsonsigned out. GraphQL introspection left on publishes your entire schema.
A coding agent has no way to know which of your variables is a secret. It sees a string that makes the fetch work, puts it where the fetch can reach it, and the fetch works. Nothing in the loop ever asks whether the browser should have been able to read it.
The three you want in writing
Not exploitable on their own, mostly. Worth closing anyway, because they are cheap, and because the first enterprise customer who sends you a security questionnaire will ask about all three.
- 01Session cookies are
HttpOnly,SecureandSameSite. A session cookie readable by JavaScript turns any script injection into a full account takeover instead of a defacement. - 02CORS does not reflect any origin with credentials. An
Access-Control-Allow-Originthat echoes whatever origin asked, together withAllow-Credentials: true, lets any website read your API as your logged-in user. - 03Your domain cannot be spoofed in email. Missing SPF and DMARC records mean anyone can send mail that appears to come from your company. This is the one on the list your customers will be attacked through, not you.
What a pass actually entitles you to say
Twelve out of twelve, checked by hand, honestly, is a real achievement and it still does not mean "secure". It means: nothing visible from outside is exposed, and the four authorization checks passed for the specific records you tried.
The distinction matters because it is the one people get wrong in both directions. Founders who pass an external scan believe they are covered when authorization was never tested. Founders who find nothing believe the exercise was pointless, when a clean external surface written down is exactly what you want on file before you ship.
If you ran the eight external checks and skipped the four that need two accounts, the best true statement is "nothing is exposed from the outside, and authorization is untested". That is also the best verdict a ShipSafe Preflight will return you, for the same reason. We would rather lose the compliment than earn it dishonestly.
Fix, then prove the fix
The half of this that founders skip is the second half. A change that looks like a fix is not a fix; a fix is when the request that worked twenty minutes ago stops working. Those are different claims, and only one of them is checkable.
So for every item you close, keep the exact request that demonstrated the problem, deploy the change, and send that same request again. If it still succeeds, you fixed something adjacent. If it now refuses, you are done — and you have a before and an after you can show a customer.
This is the whole loop, and it is the only part of security work that produces evidence instead of opinion: detect, prove, fix, replay, and let the status change only when reality changes.