Launch day is the worst time to discover a security problem and the most common time to ship one. Here is the fast pass to run before you hit deploy. It will not make you SOC 2 compliant. It will catch the things that actually get small apps breached.
A word on the 60 seconds in the title, because it deserves honesty. A minute is roughly what the automated part costs you: point a scanner at your source and read the verdict. The manual checks in this post take longer, closer to an hour the first time you do them. You need both, because they answer different questions, and the first section explains which is which.
Can a 60-second scan tell you your app is safe to ship?
No. A one-minute automated scan can tell you whether known, pattern-shaped problems exist in the source you gave it: committed secrets, dependencies with published vulnerabilities, risky configuration, and common auth mistakes. It cannot see your deployed settings or judge your business logic. A clean result means nothing obvious was found, which is useful and is not the same as secure.
What a fast scan is good at:
- Secrets in files and in git history. A machine reads every commit. You will not.
- Dependencies with known vulnerabilities. This is a lookup against published advisories, which is exactly what software is for.
- Configuration patterns in code. A debug flag left on, a wildcard CORS origin, a cookie set without its security attributes, a token decoded and never verified.
- Consistency. It checks every file, every time, including the one you wrote at 2am.
What it cannot see:
- What is actually deployed. The database rules live in a console, the environment variables on your host, the headers your CDN adds or strips. None of that is in the repository.
- Whether user A can read user B's data. Authorization logic is specific to your app. A scanner can notice a route with no auth check. It cannot know that invoice 123 belongs to someone else.
- Whether a leaked key was rotated. It can find the key in an old commit. Only the provider's dashboard knows if it still works.
- Third-party settings. Billing caps, API key restrictions, who has admin on your hosting account.
So the honest shape of a pre-launch audit is one minute of machine time to clear the mechanical findings, then a short list of manual checks aimed at what the machine cannot reach. The rest of this post is that list. Every check works whether or not you ever install a scanner.
How do you check for exposed API keys before launch?
Search three places: your source, your git history, and the bundle your build produces. A key deleted from the current files can still sit in an old commit, and a server-only key can end up inlined into client JavaScript by a build tool. Anything that was ever committed or shipped should be rotated, not just removed.
Search your source and your built bundle for keys, tokens, and high-entropy strings. If a secret was ever committed, rotate it. This check is cheap and the problem it catches is extremely common: GitGuardian's State of Secrets Sprawl 2026 report counted 28.65 million new hardcoded secrets added to public GitHub commits in 2025 alone.
# History: every commit that added or removed the string
git log --all -p -S"PASTE_A_FRAGMENT_OF_THE_KEY"
# Build output: server-only names that should never reach a browser
grep -rnE "service_role|sb_secret_|PRIVATE KEY|SECRET_KEY" dist/
Adjust the second pattern to the providers you use and to wherever your build writes its output. The first command matters because deleting a line does not delete the commit that added it, which is the whole subject of why a deleted API key is still in your git history.
Not every key in a bundle is a finding. Some are public by design. Firebase's documentation says API keys restricted to Firebase services "do not need to be treated as secrets," and a Supabase anon or publishable key is in the same category. What must never reach a browser is anything that bypasses your access rules or spends money: a service role key, a service account file, a payment secret key, an LLM provider key.
How do you know your database is locked down?
Make a request with no credentials and confirm it fails. Reading your rules tells you what you configured. An unauthenticated request against the live project tells you what a stranger actually gets, which is the only answer that counts. Then sign in as one test user and try to read another user's record.
Confirm your Firebase, Supabase, or database rules deny by default and scope access to the owner. This is the check to spend the most time on. Broken Access Control is the first category in the OWASP Top 10:2025, and it is the one a source scanner is least able to settle for you. The plain-language tour of that list is in the OWASP Top 10 for solo developers.
# Firestore: expect PERMISSION_DENIED, not documents
curl "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/users"
# Supabase: expect an empty array or an error, never rows
curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: YOUR_PUBLISHABLE_KEY"
If you started a Firebase project in test mode, assume nothing. Firebase's own quickstart describes that mode as one that "allows anyone to read and overwrite your data," and the usual way people get past its expiry is to make the open rule permanent. The details are in when Firebase security rules are open by default.
If you have your own API, do the same test one layer up. For every route that takes an ID, sign in as one user and request an ID that belongs to another. A 200 with someone else's data is the most valuable finding on this page.
Auth: is it verifying, not just decoding?
Check that tokens are verified, secrets are strong with no defaults, cookies use Secure and SameSite, and sessions expire. Four specific things to look for:
- Decode where verify should be. The README for the Node
jsonwebtokenlibrary warns thatjwt.decode"will not verify whether the signature is valid" and that you most likely wantjwt.verify. Search your server code fordecode(and check every hit. - A fallback signing secret. Search for patterns like
process.env.JWT_SECRET || "secret". If the variable is missing in production, the fallback is your signing key, and it is in your repository. - Cookie attributes. MDN's reference is the short version: a
Securecookie is only sent over HTTPS, anHttpOnlycookie cannot be read by JavaScript, andSameSitecontrols whether it is sent with cross-site requests. Session cookies want all three. - Expiry. Tokens should carry an expiry and sessions should end. A token that works forever is a password the user cannot change.
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Lax; Path=/
The longer list of ways this goes wrong, with examples, is in JWT authentication mistakes that ship to production.
Safe to ship is not a feeling. It is a short list of checks that either pass or do not.
Config: anything left in debug?
No debug mode in production, no source maps served publicly, security headers present, no default credentials. These are careless mistakes, which means they are the easiest to fix. They are also common enough that Security Misconfiguration sits second in the OWASP Top 10:2025.
Check the deployed site, not the config file, because your host or CDN may change what is actually sent:
# Response headers on the live site
curl -sI https://yourapp.example
# Is a source map being served? Expect a 404
curl -sI https://yourapp.example/assets/index.js.map
In the first response, look for the headers the OWASP HTTP Headers Cheat Sheet recommends: Strict-Transport-Security, X-Content-Type-Options: nosniff, a Content-Security-Policy, and either X-Frame-Options: DENY or a CSP frame-ancestors directive. The same cheat sheet recommends removing X-Powered-By, which tells an attacker what you are running for no benefit to you. A publicly served source map can hand over something close to your original source.
Then the quick ones. Is the framework's debug or development mode off in the production environment? Does any admin account still use the password from the setup guide? Does an error page print a stack trace?
Dependencies and spend: the two checks people skip
Dependencies first, because it is one command. The npm documentation describes npm audit as submitting a description of your project's dependencies to the registry and asking for a report of known vulnerabilities:
npm audit --omit=dev --audit-level=high
That limits the report to what ships and to the findings worth stopping a launch for. Other ecosystems have an equivalent. Software Supply Chain Failures is third in the OWASP Top 10:2025, and this is the cheapest item on the list to act on.
Spend is the check almost nobody runs. If any endpoint calls a paid service (an LLM, SMS, email, image generation) and can be reached without signing in or without a rate limit, someone else can run up your bill. Before launch, put a rate limit on every such route, require authentication where you can, and set a budget alert or hard cap in the provider's dashboard. The mechanics are in denial of wallet and the serverless bill.
What does "safe to ship" actually mean?
It means a defined list of checks passed and you know what was left unchecked. It does not mean the app has no vulnerabilities, and no scanner or audit can honestly promise that. For a small app the list is short: no live secrets exposed, data denied by default, auth verified, production config, known-vulnerable dependencies patched.
The whole pass, in order:
- Run an automated scan across your source and git history. Fix or consciously accept each finding.
- Search your build output for server-only keys. Rotate anything that was ever committed or shipped.
- Make an unauthenticated request to your database and confirm it is refused.
- Sign in as one test user and try to read another user's data, in the database and through your API.
- Confirm tokens are verified, cookies carry their attributes, and no signing secret has a default.
- Check the live response headers, and confirm debug mode and source maps are off.
- Run your dependency audit and patch the high and critical findings.
- Put a rate limit and a spending cap on anything that costs money per request.
Make it a habit, not an event. The reason this pass gets skipped is that doing it by hand is tedious. Automate the half that can be automated and you are left with a handful of manual checks that fit in a release routine. Run the scan on every release. Run the manual checks whenever you add a table, a route or a paid API.
Running the automated half with IOnclad
That first step is the one IOnclad exists for. The free IOnclad browser scanner runs a 167-rule subset entirely in your browser, with no signup and no email wall. Your code stays in the tab. It is a quick way to find out whether anything obvious is sitting in your source.
The IOnclad desktop app goes wider: 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, so the answer to "am I safe to ship?" arrives as a list you can work through. If you are weighing it against tools you already know, here is how IOnclad compares with Snyk, Semgrep and Trivy.
It does not promise your app is secure, and no scanner honestly can. It reads your code. It does not log in as a second user or look at your Firebase console, which is why the manual checks above stay on the list. For more of those, the app security guides collect the rest in one place. Ask the question and get an honest answer before your users do.