← Back to blog

The Pre-Launch Security Checklist for Apps Built with AI Coding Tools

You used Cursor, Lovable, Bolt, Replit, or Claude Code to get this far, and it worked. The app runs, the demo looks good, and you're close to sending people a link. Before you do, run through the six checks below. Each one takes under ten minutes, uses tools you already have, and tells you something concrete: not "AI code is risky" in the abstract, but whether your specific repo, your specific database, and your specific API keys are exposed right now.

None of this requires a security background. It requires running a command and reading the output.

Why this list, and why now

AI coding tools are very good at making something work and quiet about making it safe. A prompt like "add Stripe checkout" gets you working code fast, but the model has no way to know your database's row-level security is still wide open, or that the API key it pasted into a config file will sit in your git history forever. These are specific, repeated failure modes, not hypotheticals.

Work through these in order. Each section tells you what to run, what a clean result looks like, and what to do if it isn't clean.

1. Secrets committed to your repo

This is the most common first mistake, and the easiest to check. If an API key, database password, or access token ever got typed into your code, even in a file you later deleted, it can still be sitting in your git history, readable by anyone who clones the repo.

Run this from your project root to check your commit history:

git log -p --all | grep -niE "sk-|sk_|AIza|AKIA|ghp_|xox[baprs]-"

And this to check your current working tree, including files you haven't committed yet:

grep -rniE "sk-|sk_|AIza|AKIA|ghp_|xox[baprs]-" --exclude-dir=node_modules --exclude-dir=.git .

Those patterns catch the most common key formats: AKIA for AWS access keys, ghp_ for GitHub personal access tokens, AIza for Google API keys, and the xox family for Slack tokens. The sk- prefix covers OpenAI and Anthropic keys; sk_ (with an underscore) covers Stripe secret keys, which look like sk_live_... and sk_test_.... Both sk patterns are broad and will catch some false positives (variable names, hashes) alongside real keys, so read the matches rather than trusting the count.

Before you trust a clean result, prove the check actually works. Paste a fake key into a scratch file, like const test = "AKIAABCDEFGHIJKLMNOP";, run the second command again, and confirm it finds that line. Then delete the file. If your "clean" scan can't even find a key you just planted, the scan is broken, not your code. This matters more than it sounds: a grep command with one wrong character can silently search zero files and report nothing found, which looks identical to an actual clean pass.

If either command finds something, the fix isn't just deleting the line and committing again, because the key is still in your history. Revoke and rotate the key at the provider first, that's the only step that actually neutralizes the exposure, then remove it from history with a tool built for that, such as git filter-repo. Rotating matters more than scrubbing history. History-scrubbing helps if the repo is public or gets shared later; rotating the key protects you the moment it happens.

Also confirm .env itself was never committed:

git ls-files | grep -E "\.env$"

If that returns a path, your .env is tracked by git, which means every key inside it is in your history regardless of what your .gitignore says today. Adding a file to .gitignore after it's already tracked does nothing retroactively.

2. A database anyone can read

If you're using Supabase, Firebase, or a similar backend-as-a-service, the database ships with row-level access control that has to be turned on and configured per table. AI coding tools are good at generating the table and the query, and inconsistent about generating the access policy that restricts who can run that query. The result is a working app where any user, or anyone with your public API URL, can read or write rows that were never meant to be visible.

For Supabase, run this in the SQL editor:

select tablename, rowsecurity from pg_tables where schemaname = 'public';

rowsecurity is a real Postgres column: true means row-level security is enabled on that table, false means it isn't. Every table holding user data should read true. If any read false, that table has no row-level restriction at all: any authenticated (or in some setups, any anonymous) request can read or write every row.

For Firebase, the equivalent check is reading your actual deployed security rules in the Firebase console, not the rules file in your repo. The two can drift if a change was made in the console and never pulled back into the codebase.

Turning on row-level security isn't enough by itself if the policy is wrong. A table with RLS enabled but a policy of "allow all" is functionally identical to no RLS at all. Read the actual policy text, not just whether one exists.

3. API keys that never expire and nobody is watching

A common story: a developer hardcodes an API key while prototyping, assumes the project is throwaway, and forgets about it. Weeks later a billing alert, or no alert at all, reveals a pile of unexpected calls against that key. The key wasn't stolen through anything sophisticated. It was sitting in code, doing exactly what a valid key does when someone else has it.

Two separate fixes here, and both matter:

Rotate long-lived keys. If a key has been sitting in your codebase for weeks, treat it as already exposed and generate a new one, even if you found no evidence of misuse. You can't prove a negative from a git log.

Set a spend limit before you need one. Every major API and cloud provider (Google Cloud, OpenAI, Anthropic, AWS) has a billing or usage-limits section in its console where you can cap spend or get alerted well before a runaway loop or a leaked key turns into a bill you didn't expect. Find that setting for every paid API your app calls and set it now, while it costs you nothing, rather than after the first surprise invoice.

4. Authorization that only exists in the frontend

This one is subtle and it's the cause of some of the largest real incidents in AI-built apps. The failure looks like this: your UI correctly hides an admin button, a delete action, or another user's data from people who shouldn't see it. But if the actual permission check only happens in the frontend, and the backend endpoint does whatever it's told, then anyone who calls that endpoint directly, which takes nothing more than opening browser dev tools or using curl, bypasses the check entirely.

To check this yourself: pick your three most sensitive actions (viewing another user's data, an admin-only action, a paid feature). For each one, log in as a low-privilege or anonymous user and try to trigger that action directly against the backend, not through the UI. You can do this with curl:

curl -s -o /dev/null -w "%{http_code}\n" https://yourapp.com/api/admin/users

A 401 or 403 is what you want to see. A 200 means the endpoint answered, which means the check either doesn't exist or isn't being enforced server-side. The frontend hiding a button is a UX decision. The backend refusing the request is the actual security control.

5. Dependencies nobody has looked at

AI coding tools install packages the same way a human would: they add whatever solves the immediate problem. Nobody sits down afterward and checks whether any of those packages have a known vulnerability. This is a five-second check:

npm audit

To scope the check to what actually ships, skipping dev-only tooling that never reaches production:

npm audit fix --omit=dev

Read what it finds before blindly accepting fixes. npm audit fix sometimes wants to bump a major version, which can break your app; check the changelog for anything it proposes upgrading past a major version boundary before running it unattended.

6. Debug output and permissive config left on

Fast iteration during development often means verbose error pages, permissive CORS, and debug flags left on, because they made development easier and nobody flipped them off before shipping. Two quick checks:

Stack traces in production. Visit a route on your live app that you know will error (an invalid ID, a malformed request) and see what comes back. A raw stack trace or internal file paths in the response body means debug mode is likely still on, and it's telling an attacker more about your app's internals than you'd choose to.

Wildcard CORS. Check your server's CORS configuration for Access-Control-Allow-Origin: * set alongside Access-Control-Allow-Credentials: true. That combination is rejected by browsers per the CORS spec, because it would otherwise let any website make authenticated requests to your API on a logged-in user's behalf and read the response. If you find both set, replace the wildcard with an explicit list of the origins you actually control.

After you've run these

Six checks, one afternoon, and you now know something concrete about your own app instead of a general worry about AI code. If a check came back dirty, fix that one before you ship. The list stays useful every time you add a feature, because the same failure modes come back with the same prompts.

If you would rather these run for you against your own code, there is a free browser-based IOnclad scanner at theionproject.com/ionclad/scan/. It runs in the browser and asks the one question this whole list is about: are you safe to ship.

Try it free
IOnclad

Scan your app for the issues that stop a launch.