ShipSafe

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.

Read this before you start

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.

§ 01

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.

  1. 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.
  2. 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.
  3. 03No admin or service key is in the client. A Supabase service_role key, 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.
  4. 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 Forbidden
The shape of check 01. If the second response returns account B's data, that is the finding — and the evidence is the response body itself.
§ 02

The 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.

  1. 01No secret is in the client bundle. Any variable prefixed NEXT_PUBLIC_, VITE_ or REACT_APP_ is compiled into the JavaScript the browser downloads. Read the list and ask, for each one, whether you would post it publicly.
  2. 02No .env or .git is served. Request /.env and /.git/config on your live domain. Both should 404. A served /.git directory can reconstruct your whole source history, including the secrets you deleted later.
  3. 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.
  4. 04Source maps are not published in production. They hand over your original source, comments included. Useful in staging, an unnecessary gift in production.
  5. 05No debug, admin or docs route is open. Try /admin, /debug, /api/docs, /graphql and /openapi.json signed out. GraphQL introspection left on publishes your entire schema.
Why this class is so common

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.

§ 03

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.

  1. 01Session cookies are HttpOnly, Secure and SameSite. A session cookie readable by JavaScript turns any script injection into a full account takeover instead of a defacement.
  2. 02CORS does not reflect any origin with credentials. An Access-Control-Allow-Origin that echoes whatever origin asked, together with Allow-Credentials: true, lets any website read your API as your logged-in user.
  3. 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.
§ 04

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.

The honest verdict

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.

§ 05

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.

Questions people ask

Short answers, no hedging

What should I check before launching a vibe-coded app?
In this order: secrets in the client bundle, exposed files such as .env and .git, whether one logged-in account can read another's data, whether your API returns more fields than the UI shows, file upload handling, and finally headers and cookie flags. The order matters, because the first three can lose customer data and the last one usually cannot.
How long does a pre-launch security check take?
The passive half, which is everything visible from outside without logging in, takes about 1 min. The part that needs accounts and crafted requests takes longer, roughly 25 min when it is automated. Fixing what it finds is the slow half, and it is your coding agent's work rather than the test's.
Do free security scanners find real problems?
They find real facts and few real problems. A scanner reports that a header is missing or a library version is old, without showing that anything can be exploited or that a fix worked. What you need before launch is a reproduced request, the response it returned, and a retest that can no longer reproduce it.
Can I just ask Cursor or Claude Code whether my app is secure?
It is worth doing, and it is not evidence. The tool that wrote the code is reading the same code that contains the mistake, and it has no view of your deployed configuration, your database policies, or what your live API actually returns to an unauthenticated request. Use it to fix findings, not to certify their absence.
Run the check on your own app

Stop reading about it. Point us at your URL.

The free Preflight is passive and read-only. It checks your app for exactly the exposures this guide describes and returns a score, a verdict and one finding with its evidence. No signup, no card.

Keep reading