← Back to blog

The Pre-Launch Security Checklist Indie Developers Actually Need

Most security advice is written for teams with a security budget. If you are one person shipping this weekend, you do not need a 200-item audit. You need to catch the handful of mistakes that actually get small apps breached or billed, using nothing more than a terminal and a browser.

This is that list. It covers three overlapping situations: a solo project of any kind, an app that Cursor, Lovable, Bolt, Replit or Claude Code wrote most of, and a backend or API you are about to expose to the internet. Each check says what to run and what a clean result looks like.

What should a solo developer check before launching an app?

Check six things before launch: no secrets in your source, git history or built bundle; database rules that limit each row to its owner; authorization enforced on the server for every endpoint; rate limits and spending limits on anything that costs money; production config with debug off and headers on; and dependencies audited and pinned.

The order runs from the failures that happen most often and cost the most to the ones that are cheaper to fix late. If you only have an hour, start at the top. A leaked key or an open table hurts on day one. A missing header usually does not.

You do not have to be a security expert. You have to not ship the obvious mistakes.

How do you find secrets in your code, git history and built bundle?

Search three places with the same pattern: your working tree, every commit on every branch, and the build output a browser downloads. A key that was ever committed is burned even if you deleted it later, so rotate it at the provider first and clean up second. Finish by confirming no .env file is tracked.

A hardcoded API key, database URL or token is the most common way a small app leaks. GitGuardian's State of Secrets Sprawl 2026 report counted 28.65 million new hardcoded secrets in public GitHub commits during 2025, up 34% on the year before, and found that commits co-authored by Claude Code leaked secrets at roughly twice the baseline rate. The mechanics are covered in how indie apps leak their API keys.

From your project root (use Git Bash on Windows):

PATTERN='AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|github_pat_|AIza[0-9A-Za-z_-]{35}|sk_live_|rk_live_|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-|sb_secret_|BEGIN [A-Z ]*PRIVATE KEY'

# every commit on every branch
git log -p --all | grep -nE "$PATTERN"

# the working tree, including files you have not committed
grep -rnE "$PATTERN" --exclude-dir=node_modules --exclude-dir=.git .

# the build output (adjust the folder: dist, build, .next, out)
grep -rnE "$PATTERN" dist/

Those patterns cover AWS access key IDs (AKIA), GitHub tokens, Google API keys (AIza), Stripe live secret and restricted keys, the sk- family used by OpenAI and Anthropic, Slack tokens, Supabase secret keys and private key files. They are a starting set, not every format, so read the matches instead of trusting the count.

Prove the check works before you trust a clean result. Paste a fake key into a scratch file, such as const test = "AKIAABCDEFGHIJKLMNOP";, run the working-tree command again and confirm it finds that line. Then delete the file. A grep with one wrong character can search nothing and report nothing, which looks identical to a real pass.

If something real turns up, deleting the line and committing again does not fix it, because the secret is still in your git history. GitHub's guidance on removing sensitive data puts revoking or rotating the secret first, and notes that rotation alone may be enough. Rewriting history with git filter-repo is the second step, worth doing if the repository is public or will be shared. Apply the same rule to any key that has sat in the codebase for weeks: rotate it even with no sign of misuse, because you cannot prove from a git log that nobody copied it.

Then confirm no environment file is, or ever was, tracked:

git ls-files | grep -E '(^|/)\.env'
git log --all --oneline -- '*.env*'

Output from either command means the values in that file are in your history, whatever .gitignore says today. Ignoring a file after it is tracked changes nothing retroactively.

Last, leave GitHub's safety net on. According to GitHub's documentation, secret scanning runs automatically and free on public repositories, and push protection for your user account is enabled by default on GitHub.com. A blocked push costs you a minute.

How do you check that your database is not open to anyone?

Do not read the config and assume. Ask the database the way a stranger would: take the public key out of your own frontend, request a table that should be private without logging in, and confirm nothing comes back. Then repeat it for a write. Rows in the response are a finding, whatever the dashboard says.

Backend-as-a-service products such as Supabase and Firebase move authorization out of your server code and into a rules layer. The client key is public by design, so the rules are the only thing between a visitor and your data.

Supabase

In the SQL editor, list every table in the public schema with its Row Level Security status:

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

Every row should read true. Supabase's documentation says to enable RLS on every table in an exposed schema. Then test from outside, using the publishable or anon key from your own bundle:

curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

An empty array or a permission error is the result you want. RLS being on is not the end of the check, because a policy whose condition is simply true restores the access you just removed. The ways RLS can be on and still not protect anything are covered in whether the Supabase anon key is safe to expose.

Firebase

Read the rules that are deployed in the Firebase console, not the rules file in your repo. The two drift when someone edits in the console. Firebase's documentation on insecure rules warns against ever shipping allow read, write: if true, and also flags a bare auth != null check, because that gives every signed-in user access to everything. Tie each document to its owner instead:

match /users/{userId} {
  allow read, write: if request.auth != null && request.auth.uid == userId;
}

The Firebase security rules post walks through the default that catches people.

What should you check before launching an app built with AI coding tools?

Run the same checklist as everyone else, plus three checks for the gaps these tools leave: permission checks that exist only in the frontend, packages the model invented that may not be what they claim, and quick fixes that removed an error by removing a protection. A working demo tells you nothing about any of them.

AI coding tools are very good at making something work and quiet about making it safe. The prompt was "add checkout", not "add checkout and make sure one customer cannot read another customer's orders", and the model cannot see your deployed rules or your git history. The CVE record for CVE-2025-48757, published on 30 May 2025, describes insufficient Row Level Security policies in apps generated by Lovable through 15 April 2025, which let remote unauthenticated attackers read or write arbitrary database tables.

Permission checks that only exist in the frontend

Your UI hides the admin button from people who should not see it. If the endpoint behind that button does whatever it is told, anyone with dev tools or curl skips the UI entirely. Pick your three most sensitive actions and call each one directly with no credentials:

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

You want 401 or 403. A 200 means the server never checked. Hiding a button is a UX decision. The server refusing the request is the security control.

Packages the model made up

Models sometimes import a plausible package that does not exist, and attackers register those names on purpose, a tactic known as slopsquatting. A USENIX Security 2025 study by Spracklen and colleagues generated 576,000 code samples across 16 models and found that hallucinated packages averaged at least 5.2% for commercial models and 21.7% for open-source ones. For every dependency you do not recognise, run npm view PACKAGE_NAME and look at the publish date, the maintainers and the repository link before you trust it.

Fixes that removed the error by removing the protection

When an assistant hits a permissions error, the shortest path to a passing run is often to switch the control off. Search your code for three of these: a database policy of using (true), a secret renamed with a NEXT_PUBLIC_ or VITE_ prefix so the client could read it (those prefixes inline the value into the browser bundle), and CORS code that echoes back whatever origin called. Why generated code lands here so reliably is covered in the security risks of vibe coding.

What should you check before making a backend public?

Check four things before an API is reachable from the internet: every endpoint confirms the caller may touch that specific record, not just that they are logged in; anything that costs money per call is rate limited; input that reaches a query or a prompt is treated as untrusted; and every exposed route is one you meant to expose.

The dedicated backend security checklist walks through each of these in more depth.

Authentication and authorization are different checks

Authentication asks who you are. Authorization asks whether you may touch this particular record. The classic gap is an endpoint that confirms you are logged in and then returns any row you request by ID, including rows that belong to someone else. OWASP's API Security Top 10 ranks this first, as API1:2023 Broken Object Level Authorization. Test it with two accounts:

# signed in as user A, asking for a record that belongs to user B
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer USER_A_TOKEN" \
  https://api.yourapp.com/orders/USER_B_ORDER_ID

You want 403 or 404. If user B's data comes back, put the owner into the query itself:

// before: any signed-in user can read any order
const order = await db.order.findUnique({ where: { id: req.params.id } });

// after: the owner is part of the lookup
const order = await db.order.findFirst({
  where: { id: req.params.id, userId: req.user.id }
});
if (!order) return res.status(404).end();

While you are in the auth code, check the token handling. Decoding a JWT is not verifying it. Both hand you a user ID, and only one checks the signature. The README for the widely used jsonwebtoken library warns that jwt.decode does not validate the signature, so anything guarding a route must call jwt.verify. Confirm sessions expire, and that password reset gives the same response whether or not the account exists. The details are in the JWT authentication mistakes post.

Rate limits and the surprise bill

You do not have to be breached to be hurt. Any public endpoint that calls a paid API, sends an email, runs an LLM prompt or writes to storage can be looped by a script until the invoice has extra digits. Put a limit in front of everything that costs money per call, and a lower one in front of anything unauthenticated:

import rateLimit from "express-rate-limit";

app.use("/api/generate", rateLimit({ windowMs: 60_000, limit: 10 }));

On serverless platforms an in-memory limiter resets with every new instance, so use a shared store or the platform's own rate limiting. Then set billing controls on every paid service. An alert is not a cap: Google Cloud's documentation states that an alerts-only budget does not stop usage or billing when the threshold is passed. Where a provider offers a hard spending limit, set it. Where it does not, enforce a per-user quota in your own code. The full pattern is in how denial of wallet runs up a serverless bill.

Input on every public route

Anything a public route accepts is untrusted. Look for places where user input is concatenated into a query:

// injectable: user input becomes part of the SQL text
db.query(`select * from users where email = '${email}'`);

// parameterised: user input is only ever data
db.query("select * from users where email = $1", [email]);

For document databases, validate that each field is the type you expect, since a JSON object arriving where you expected a string can change what a query means. If user text reaches an LLM, assume it can override your instructions. OWASP's Top 10 for LLM Applications lists prompt injection first, as LLM01:2025. The system prompt is not a security boundary, so the defence is limiting what the model's output is allowed to do: which tools, which data, which actions without confirmation.

What is actually exposed

Get a plain list of every route and confirm each one is meant to be reachable. Most frameworks will print it: rails routes, flask routes, php artisan route:list, the route table in next build output, or the /openapi.json that FastAPI serves by default (itself a route to decide on). Debug routes, seed scripts and internal admin endpoints ship by accident more often than by design.

Deploy config: check the running app, not the repo

The code can be fine while the deploy leaks, and the running configuration can disagree with the repo. OWASP's Top 10:2025 moved Security Misconfiguration up to second place, from fifth in 2021. Run four checks against the live URL:

  • Debug output. Request a route you know will error, such as an invalid ID. A stack trace or an internal file path in the response means debug mode is still on.
  • CORS. Send a request with a foreign origin and read the response headers. MDN documents that browsers block a wildcard origin combined with credentials, so the dangerous pattern is the workaround: a server that echoes any origin back alongside Access-Control-Allow-Credentials: true. Use an explicit list of origins you control.
  • Headers and HTTPS. Look for strict-transport-security, content-security-policy and x-content-type-options, and confirm the plain http:// URL redirects to https://.
  • Source maps and storage. Confirm .map files are not served in production, and that no storage bucket is public read unless it holds only public assets.
# response headers, then the plain-http redirect
curl -sI https://yourapp.com
curl -sI http://yourapp.com

# does the API accept an origin it has never heard of?
curl -s -o /dev/null -D - -H "Origin: https://example.org" \
  https://api.yourapp.com/me | grep -i access-control

If you deploy with Docker or Terraform, the same class of problem lives one layer down, in insecure infrastructure-as-code defaults.

Dependencies: audit what ships

Your package.json is an attack surface. OWASP's Top 10:2025 lists Software Supply Chain Failures third, an expansion of the older vulnerable-components category. Run:

npm audit --omit=dev

The --omit=dev flag scopes the report to what reaches production. Read the findings before fixing anything. Per the npm documentation, npm audit fix stays inside your declared version ranges, and only npm audit fix --force installs semver-major updates. Those can break your app, so read the changelog first. Commit your lockfile and deploy with npm ci, so production installs exactly what you tested.

The master checklist, top to bottom

Run it in order before you point a domain at anything.

  1. Plant a fake key and confirm your grep finds it, then search the working tree, the full git history and the build output.
  2. Confirm no .env file is tracked or sitting in history.
  3. Rotate every key that was ever committed, or that has sat in the codebase for weeks.
  4. Confirm RLS is on for every Supabase table, or read the deployed Firebase rules, and read each policy for conditions that are always true.
  5. Request a private table with the public key and no login, once for a read and once for a write.
  6. Call your three most sensitive endpoints with no credentials. Expect 401 or 403.
  7. Sign in as user A and request user B's record by ID. Expect 403 or 404.
  8. Confirm tokens are verified, not just decoded, sessions expire, and password reset does not reveal whether an account exists.
  9. Rate limit every endpoint that costs money per call, with a lower limit for unauthenticated callers.
  10. Set a billing alert and, where the provider offers one, a hard spending limit on every paid service.
  11. Read every place user input reaches a SQL query, a document query or an LLM prompt.
  12. Print the route list and remove debug, seed and admin routes that should not be public.
  13. Trigger an error on the live app and confirm no stack trace comes back.
  14. Check CORS, security headers, the HTTPS redirect, source maps and bucket permissions against the live URL.
  15. Run npm audit --omit=dev, look up every package you do not recognise, and commit the lockfile.
  16. If an AI tool wrote the code, search for using (true), secrets behind NEXT_PUBLIC_ or VITE_ prefixes, and CORS that echoes any origin.

Fix anything that comes back dirty before you ship, and rerun the list when you add a feature, because the same failure modes come back with the same prompts.

Scanning for this before you ship

You can run all of the above by hand, and for one small app that is a reasonable afternoon. If you would rather have something read your source and hand you a ranked list first, the free IOnclad browser scanner runs a 167-rule subset in your browser, with no signup. Your code stays in the tab.

The IOnclad desktop app runs 24 scanners and 500+ checks across secrets and git history, dependencies, the OWASP web categories, session and auth, an API surface map, and a denial-of-wallet check. It returns one Ship-It verdict with the file, line and a fix for each finding. It does not promise your app is secure, and no scanner honestly can. What it gives you is evidence for a decision you are about to make anyway.

This post is part of our app security guides for indie developers.

Try it free
IOnclad

The pre-launch security scanner that answers "Am I safe to ship?" in under a minute. Runs 100% offline, free to start.

→
Keep reading
IOnclad Am I Safe to Ship? A 60-Second Pre-Launch Security Audit Read → IOnclad The OWASP Top 10, Explained for Solo Developers Read → IOnclad Hardcoded Secrets: How Indie Apps Leak Their API Keys Read →
Reading us on Google?
Add The IOn Project as a preferred source

One click on Google’s preferences page, and our articles show up more often in your Top Stories, AI Overviews, and AI Mode.

→