A launch-readiness self-check for shipping a new product or hardening an existing one, vibe-coded apps included. It covers eight areas, from the server it sits on to the bill at the end of the month.
Every item is one line: what to check and the risk. Where it helps, an item adds a short note on why it matters, an example self-test, or a couple of terms to look up. No long tool lists that depend on your stack and go out of date. Where one option is the field's de-facto standard, it's named as the default to reach for unless you have a clear reason to pick something else.
This is a floor, not a guarantee. It won't fix bad judgment or a shaky build, and ticking every box won't make a bad product good. What it will do is close the obvious holes and head off the predictable failures that sink most early products. Bring your own judgment; the list just keeps you from skipping the basics.
- Server — the machine your product runs on
- Repo — your code and its history
- Pipeline (CI/CD) — how code gets checked and shipped
- Stack — the data layer underneath
- Application security — where user data leaks
- Agentic pipelines — agents that write your code, and agents inside your product
- Code quality — staying workable as it grows
- Cost — no surprise bill
By hand. Go through each item and mark its state:
[x] handled · [~] partial / not sure · [ ] not done
With your coding agent. Point it at this repo (clone it, or paste in the raw file) and have it audit your code and infra against the items. An example prompt to start from:
This repo has a launch-readiness checklist. Spawn one subagent per section, each auditing my codebase and infra against every item in its section thoroughly. Then assemble a filled-in local copy of the checklist (marked
[x]/[~]/[ ]), and give me an overview of the most critical gaps.
★ marks items where a slip leaks user data. Most other items keep your product running; these keep user data out of the wrong hands, and the stakes are a different class. A surprise $1000 bill hurts but you recover; leaked passwords or medical records are legal liability and trust you don't get back. If you ship with nothing else done, do these first:
- ★ Access control, no injection, no roll-your-own auth, secure sessions, no frontend secrets, locked-down CORS (§5)
- ★ No live secrets in git history (§2)
- ★ Locked-down server: firewall, datastore not on the public internet (§1)
The machine your product runs on.
- Non-root user — root SSH login is disabled in config (
PermitRootLogin no), and the app runs as a regular user. Root mistakes are unrecoverable. Test: runssh root@your-serverfrom your laptop; it should refuse, not ask for a password. - SSH keys only — password login over SSH disabled. Passwords get brute-forced, keys don't. Test: try to SSH from a machine without your key; you should be denied, never prompted for a password.
- Brute-force protection — repeated failed logins ban the IP (fail2ban or equivalent). Stops automated password-guessing.
- ★ Firewall — only the ports you actually use are open (ufw, or your cloud's security groups). Every open port is an entry point.
Test: scan your server from outside (
nmap your-server, or an online port scanner); only the ports you expect should answer. - ★ Datastore not on the public internet — your database and internal services (Postgres, Redis, Mongo, Elasticsearch, admin panels) bind to localhost or a private network, behind the firewall, never reachable on a public IP, and never on default or empty credentials. A database open to the internet is the single most common cause of mass data leaks.
To reach the DB from outside the service, the default is an SSH tunnel or a locked-down admin layer, never a public port. If you still really need direct access, at least make the password strong and the username, database name, and schema all non-default.
Test: from a machine outside your network, try to connect to the DB port (
psql,redis-cli, or a port scan); it should refuse, not connect. - Automatic security updates — OS patches apply without you remembering (unattended-upgrades on Debian/Ubuntu). Unpatched servers are the easiest target.
- HTTPS/TLS — valid cert with auto-renew (Let's Encrypt). No plaintext traffic, no expiry surprises.
Test: open your site with
http://; it should redirect tohttps. Check the cert expiry date while you're there. - Backups — automatic, stored off this box, and you've restored one. An untested backup is a guess.
- Monitoring & alerts — you find out the box is down or full from a tool, not from a user. Test: stop the app or fill the disk on purpose; did you actually get pinged, or did you have to spot it yourself? Search: "uptime monitoring", "error tracking", "observability".
Your code and its history.
- Secret scanning — a scanner (gitleaks, trufflehog, or your git host's built-in) blocks committed keys before they land.
- ★ No live secrets in history — anything ever committed is rotated and scrubbed from history (git-filter-repo).
- Branch protection —
mainneeds a PR, no force-push, no direct commits. Test: try togit pushstraight tomain; it should bounce. - .gitignore covers env files, build output, large binaries.
- Account hardening — 2FA on the git host, and you know who has write access.
How code gets checked and shipped.
- Linters & formatters run automatically on every change. Machines catch the boring bugs for you.
- Tests on every PR — critical paths (login, payment, core action) covered. This is how you change code without fear. Unit, integration, and e2e tests don't replace each other; ideally you have all three: logic in isolation, the seams between parts, and the full flow a user actually takes. Test: open a PR that deliberately breaks a test; the pipeline should go red and block the merge.
- Load testing — before launch, simulate traffic well above your expected peak (aim for ~10×) and see what breaks first (k6, Locust, or similar). Better to find your breaking point in a test than during the launch.
- Reproducible environment — the build runs the same everywhere, not just on your machine (Docker is the default; use something else only with a clear reason). Kills "works on my machine". Test: build and run the image on a clean machine (or in CI); it should come up without any manual setup.
- Dependency freshness — pin versions; avoid brand-new releases (give them a few days first, so supply-chain issues have time to appear) and stale ones with known vulnerabilities.
- Automated dependency updates — a bot (Dependabot, Renovate, or similar) opens update PRs, plus vulnerability alerts.
- Staging that mirrors prod — before anything ships to production, you deploy the same build to a separate staging environment that matches prod as closely as you can (same image, same config shape, same infra), and confirm the core flows actually work there, by hand or with an agent driving a real browser (Playwright). Catches the breakage that only shows up once deployed, before your users do. Test: name your staging URL; deploy your current build there and run login → main action end to end before promoting it to prod. Search: "staging environment", "smoke test", "dev/prod parity".
- Repeatable deploy with rollback — one defined action (GitHub Actions, GitLab CI, or your platform's CI) ships, and a red pipeline doesn't.
The data layer under the product. This is the part that's hardest to change later, so it's worth a closer look.
-
Do you need a database, and which type — not every app needs one. If yes, match it to how you query your data: relational (Postgres, MySQL) for structured data with relationships, document (MongoDB) for loose nested data, key-value (Redis) for simple fast lookups. Postgres is the right default for the large majority of apps; pick something else only when you can name the specific reason.
-
Don't store what you don't need — every field you take from a user is a field you can leak. Walk your tables and drop anything kept "just in case": full birth dates, raw documents, precise location, card numbers you don't charge. The safest data is the data you never stored.
-
Sensitive data encrypted at rest — payment, medical, document, and identity data is stored as ciphertext, with the key held outside the database, not in the same dump as the data. If you hold these categories, this is as critical as the ★ items. Know which encryption you actually have. The "encrypt storage" checkbox your cloud or database offers (disk encryption, TDE) only saves you from a physically stolen disk; the database decrypts everything transparently, so a leaked dump or a database an attacker got read access to is still plaintext. To cover those, your app encrypts the sensitive fields before they reach the database. TLS (§1) only protects data in transit. Passwords are hashed, not encrypted (§5).
-
Indexes — your common queries don't scan the whole table. Without an index on the columns you filter and sort by most (user IDs, emails, timestamps), a service that felt fast gets slow as data grows.
-
Migrations — schema changes are versioned, not done by hand. Editing a live schema by hand has no undo. One wrong change on real data and there's no clean way to recover.
-
Cache — hot, rarely-changing data is served from memory (Redis) instead of hitting the database every time. The database is the most common bottleneck as traffic grows; serving frequent, rarely-changing reads from memory takes load off it. Decide when the cache expires so users don't see stale values.
- ★ Don't roll your own auth — login, signup, and password reset run on a trusted library or provider (e.g. Auth0, Clerk), and the DB stores password hashes (bcrypt/argon2), never plaintext. Hand-rolled auth is where accounts get stolen. Test: name the library or provider you use; open the users table and confirm you can't read the passwords. Search: "managed authentication".
- ★ Sessions can't be stolen or forged — after login the session lives in a cookie marked
HttpOnly,Secure, andSameSite, never inlocalStoragewhere any script can read it; it expires, and you can kill it server-side (logout, "log out everywhere", revoke a stolen session). Managed auth gets you the login; this is what protects everything after it. Most account takeovers happen after sign-in, not during it. Test: log in, open devtools → Application → Storage; the session should be an HttpOnly cookie, not a token sitting in localStorage you can copy out. Search: "session hijacking", "JWT in localStorage", "SameSite cookie". - ★ Access control on every request — changing an ID in a URL or API call (
/orders/123→/orders/124) can't hand back data that isn't yours; the server re-checks "does this user own this row" on every read and write, not just in the UI. The #1 way vibecoded apps leak data. Test: log in as one user, change an ID to a record you don't own, watch what comes back. Search: "IDOR", "broken access control". - ★ No injection holes — user input is never pasted into SQL, a shell command, or raw HTML; you use parameterized queries (or better an ORM) and escape what you render.
Test: type
' OR 1=1 --into a search box and<script>alert(1)</script>into a text field, see if anything weird happens. Search: "SQL injection", "XSS", "parameterized queries". - ★ No secrets in the frontend — API keys, tokens, and private config stay server-side; nothing secret ends up in the JavaScript the browser downloads. Easy to leak by accident, since many frameworks ship env vars with a public prefix (like
NEXT_PUBLIC_) straight to the browser. Test: open browser devtools and search the page source for one of your real keys. Search: "exposed API key", "client-side secret". - Security headers — set
Content-Security-Policy,Strict-Transport-Security(HSTS),X-Frame-Options, andX-Content-Type-Options. CSP is your second line against XSS when escaping misses something; X-Frame-Options blocks clickjacking; HSTS stops a downgrade to plain HTTP. Most frameworks set all of these with one middleware (e.g. helmet). Test: run your URL through securityheaders.com, or read the response headers in devtools → Network. Search: "security headers", "Content-Security-Policy", "clickjacking". - ★ Locked-down CORS — your API allows only your own frontend origins, never
*and never just reflects whateverOriginthe caller sends. Loose CORS lets any site call your API with your users' credentials and read the response. Test: send a request withOrigin: https://evil.example; the API must not echo it back inAccess-Control-Allow-Origin. Search: "CORS misconfiguration", "Access-Control-Allow-Origin wildcard". - File upload validation (only if you accept uploads) — restrict type and size, store files outside your web root or on separate storage, and don't trust the extension. A file the browser runs as code, or a script served from your own domain, turns uploads into stored XSS.
Test: upload an
.html/.svgwith a script inside and a file whose extension doesn't match its content; confirm neither executes or is served from your main domain. Search: "unrestricted file upload", "stored XSS via upload". - Rate-limited public endpoints — login, signup, password reset, and your API cap requests per IP and per account, with basic bot protection at the edge (Cloudflare or equivalent). Otherwise people brute-force passwords and scrape you dry. (The app-traffic layer above the SSH ban in §1 Server and the AI-agent limits in §6b.) Test: can you hit login 500 times a minute with nothing stopping you? Search: "rate limiting", "credential stuffing", "web application firewall".
- No debug output in production — a crash shows users a plain error page, not a stack trace, file path, or raw SQL error, and framework debug mode is off. Leaked internals help an attacker. Test: force an error and look at exactly what the user sees. Search: "stack trace disclosure", "debug mode in production".
This can matter in two ways.
You often can't read every line an agent writes. So don't rely only on reading: keep the setup as simple as the job allows, limit the agent so it can't do damage, and give it feedback so it fixes itself.
- Start with one agent, add more only when you have to — one general dev agent (Claude Code, Codex, or Antigravity) is the right default. Lots of specialized subagents can pay off later, but adopt them too early and they add too many moving parts before that happens. Add a specialized agent only when a concrete pain justifies it: an e2e-test runner once you're tired of running them by hand, a PR reviewer in CI, a devops agent on the servers once load grows. Reach for a multi-agent orchestration framework (CrewAI, BMAD, or similar) only when that complexity gets genuinely hard to run by hand. Skills (Claude) or your platform's equivalent are a useful middle step before moving to full multi-agent.
- Least privilege — give the agent the minimum access the job needs, nothing more. An agent that only deploys gets a token scoped to read and trigger the pipeline, not edit code or settings.
- Prefer subscriptions over API keys — run agents on a flat subscription (Claude Code, Codex, and similar) instead of wiring them to a metered API key. An autonomous agent on per-token billing can drain a budget fast. If a key is unavoidable, cap its daily spend (§8).
- Guardrails the agent can't bypass — the agent physically cannot push unformatted or untested code. The pipeline gates every change, and the agent has no access to weaken it, even if an agent generated that pipeline in the first place. The pipeline comes first.
- Feedback loops — the agent can read the results of its own work: CI output, exceptions, failing tests, fed back automatically so it confirms the run was clean. Agents fix their own mistakes far better iterating than one-shotting.
- Prompt injection — untrusted user or document input can't hijack the system prompt or trigger actions. Search: "prompt injection".
- Never run raw model output — don't pipe it straight into exec, SQL, or a shell. If running agent-generated code is genuinely required by your pipeline, do it in a sandbox with stripped permissions, cut off from your real data and infrastructure.
- Least privilege for the agent's tools — the tools the live agent can call can't delete data, move money, or trigger paid actions without a limit. A hijacked or confused agent only reaches what you handed it.
- Per-user rate limits — throttle requests per user so nobody uses your bot as a free LLM. Spend caps live in §8.
- Fallback — graceful behavior when the model API is slow or down.
- Logging — prompts and outputs stored for debugging and abuse detection.
Whether the code stays workable as it grows. Bad structure doesn't break today, it slows down every change you make later.
- Beyond lint-clean — no giant files, no 300-line functions, no copy-pasted blocks, clear names, known antipatterns avoided. Linters and formatters (§3 Pipeline) catch the mechanical issues; this is the structural layer they can't see.
- Regular agentic best-practice audit — make it a habit: every so often, run an agent across the whole codebase with a prompt like "find best-practice violations and antipatterns, flag oversized files and functions". Cheap to run, and it catches the rot that piles up between features. It's the review pass you won't do by hand (ties back to §6a, how agents write your code). For a ready-made example, see Cursor's
thermo-nuclear-code-quality-reviewskill.
Not getting a surprise $3000 bill. The single home for every spending cap; §6 points here for agent keys and per-user limits.
- Hard caps & billing alerts on every paid account — cloud, DB, AI/LLM, and any API key an agent uses. A daily limit on a key stops a runaway loop emptying the account overnight.
- Cost per request — you know it, especially LLM token cost.
- Per-request & per-user ceiling — cap what any single request or user can spend, so a heavy user or a hijacked agent can't spend too much.
- Bounded autoscaling — scaling rules have an upper limit.
- No idle paid services billing quietly in the background.
Author: vladmesh