Final App Push
A launch is the moment an app stops being a demo. Everything you were willing to hand-wave — a code you hardcoded to test with, an escape function you half wrote, a rule you meant to tighten — becomes a live liability the moment the URL is public.
This skill is the pass you make before that happens. It is organized by how things actually break, not by product category, and it ends with the part that matters most: proving the fixes work by attacking the running system.
The core mindset
Most launch checklists you'll be handed (from a video, a thread) are lists of additions: add a 404 page, add meta descriptions, add HSTS. Those are worth doing and Phase 5 covers them. But they are not where apps get hurt.
Apps get hurt at the seams between "what the client says" and "what the server believes." A fast-built app usually has no server — just a browser talking straight to a database — so every assumption the UI makes is an assumption an attacker can simply ignore by not using the UI.
So the ordering here is deliberate: find what's already exposed (Phases 1–3) before polishing what's visible (Phase 5). And never mark something fixed because you edited a file. Fixed means you tried the attack and it failed.
Phase 0 — Ask what is actually being protected
Five minutes here saves you from auditing the wrong things. Get concrete answers, from the code if the user isn't sure:
- What would be bad if a stranger got it? Paid content, personal data, someone else's private info, the ability to act as someone else.
- Who are the roles, and who assigns them? The word "admin" or "officer" is a red flag worth chasing — find out how someone becomes one. If the client decides, that is almost always a finding (see Phase 1).
- What is the actual trust boundary? Not "logged-in users are trusted." Usually: strangers < signed-up users < paying users < staff. Each boundary needs enforcement somewhere the browser can't reach.
- Is anyone's data here not the user's own? Minors, customers, a member roster. That raises the stakes on anything that lists records in bulk.
Write these down. They decide what counts as critical later.
Phase 1 — Secrets that shipped to the browser
Start here. This is the highest-yield check in the whole pass, and the one
most likely to already be exploited, because it needs no skill to exploit —
just curl.
Anything bundled into client code is public. Not obscure. Public. Framework build steps inline module constants, so a value that looks like configuration in the editor is a string in a file anyone can download.
Run the scanner and read what it finds:
scripts/scan-client-secrets.sh <build-output-dir>
Then read it critically — a scanner finds patterns, you find meaning:
- Access codes, invite codes, admin/staff codes, PINs. If the client compares typed input against a constant to decide privilege, the privilege boundary is decorative. The fix is structural, not cosmetic: delete the constant, send what the user typed to the server, and let the server decide.
- API keys. Some are designed to be public (Firebase web config,
Stripe publishable keys, Mapbox tokens) — those are fine and you should say
so plainly rather than raising a false alarm. Private keys, service-account
JSON, and anything labeled
secretare not. - Endpoints or IDs for things the UI never links to. Draft content, internal tools.
Two things people get wrong when fixing this
Removing the constant is not enough. If a code was public for even a day, it is compromised — assume someone has it. You must also rotate the value in the database or service. Removing it from the bundle only stops future leakage.
Rotation is outward-facing. Changing a code that real people use to sign in affects those people. Rotate the high-privilege one (staff/admin — few people, worst consequences) and tell the user about the user-facing one with a ready-to-run fix, rather than silently locking out their members. Check first whether existing accounts break: usually membership is "an account record exists," so rotation only affects new signups. Say that explicitly, because it's the user's main worry.
Phase 2 — Injection: where untrusted text becomes code
Look for every place a value that a human typed ends up somewhere it can execute. Frameworks like React escape text by default, which lulls people into thinking they're safe everywhere. They aren't.
Read references/injection.md for the full set of sinks, payloads to test
with, and the escaping rules. The high-frequency findings:
- Hand-rolled HTML (
innerHTML, template strings). Check every interpolation, not the ones that look risky. A single unescaped field among ten escaped ones is the whole vulnerability, and it is easy to miss because the code around it looks careful. escapeHtmlthat doesn't escape quotes. The commontextContent-then-innerHTMLtrick escapes& < >but not"or'. That's safe between tags and unsafe inside an attribute.- URLs from users going into
href/src. Escaping does nothing here:javascript:alert(1)contains no HTML metacharacters. You need a scheme allowlist (http/https/mailto). Parse with the URL API rather than string matching, because browsers strip tabs and newlines inside a scheme, sojava\tscript:runs. - Values inside inline handlers (
onclick="del('${id}')"). A quote breaks straight into executable JavaScript. Prefer a data attribute plus one delegated listener. - Spreadsheet exports. A cell starting
=,+,-,@(or a leading tab/CR) executes as a formula when opened.
Sanitize on the way in and on the way out. Storing clean data protects tomorrow's rendering code; sanitizing at render protects against data that got in another way.
Phase 3 — Authorization the browser can't talk its way out of
The question to keep asking: "if someone skipped my UI entirely and called the database directly with their own account, what would they get?"
Read references/authorization.md for concrete rule patterns. What to check:
- Ownership on every read and write. Not "is logged in" — "is the owner, or is staff." The client passing an ID does not establish ownership; the server must compare against the authenticated identity.
- Bulk listing vs single reads. These deserve different permissions. One
person reading their own record is normal; one person enumerating everyone's
is a data breach. Where the platform lets you separate
getfromlist, do — and confirm the app only ever does single reads on gated collections. - Identity fields must be immutable after creation. If a user can rewrite their own name, email, or role, then any staff dashboard, export, or audit trail built on those fields can be made to say anything.
- Size limits. An unbounded document someone else's page has to download is both a cost problem and a denial-of-service.
- User input reaching a database path. In a NoSQL world "use parameterized queries" has no literal analogue — queries are structural, so there's no query-shape injection. The real risk is a value becoming part of a document path and addressing something else. Validate anything from a URL or form as a plain identifier before it becomes a path segment. Say this clearly when a user asks about SQL injection on a NoSQL app — the honest answer is "not applicable, and here is the equivalent risk."
The trap that will bite you here
Tightening rules is where you are most likely to break the working app. The classic: comparing a field that doesn't exist on older records. Strict comparison on a missing field errors, and erroring denies — so early accounts silently lose the ability to save anything.
Use the platform's default-aware accessor, and always include a regression check in Phase 6 proving normal users can still do normal things.
Phase 4 — Transport, headers, and the CSP that breaks everything
Add HSTS, X-Content-Type-Options: nosniff, Referrer-Policy,
Permissions-Policy, and a Content-Security-Policy. See
references/headers.md for a working starting policy and the per-service
origins.
CSP is the one that silently breaks features, so:
- Enumerate every third-party origin the app actually uses before writing the policy — auth popups, fonts, analytics, video embeds, storage, bot protection. Missing one produces a blocked request and no visible error.
frame-ancestors 'self', notDENY, if the app frames its own pages. Clickjacking protection that also breaks your own embedded view is a regression, not a fix.- Load the site in a real browser afterward and check the console. A CSP that looks right and blocks a critical script is worse than none, because it fails quietly.
Phase 5 — Launch readiness
This is where those checklist items belong. They matter for polish, SEO, and
trust; they are not security. Work through
references/launch-readiness.md — custom 404, thank-you page, breadcrumbs,
per-page titles and descriptions, Open Graph image, robots.txt, sitemap,
structured data, alt text, privacy policy, analytics wiring, sticky mobile CTA,
contact route with a stated response time.
Two judgment calls worth making every time:
Never fabricate social proof. "Add testimonials / reviews / case studies" appears on these lists constantly. Inventing a customer quote, a star rating, or a named person with a title puts a fake endorsement on a real site under the user's name. Build the section, leave it empty or omit it, and ask for real material. Apply the same eye to anything already in the app from a template — placeholder testimonials with invented names ship to production more often than you'd think, and they read as real.
Delete dead affordances. Links to #, forms that swallow input, buttons
that do nothing. A form that silently discards a submission is worse than no
form, because someone believes they contacted you.
Phase 6 — Prove it by attacking
This is the part that separates a real audit from a plausible one. Do not report anything as fixed because you edited the code.
For each finding, write a check that:
- Performs the actual attack against the running system, the way an attacker would — hitting the API/database directly, not through the UI. Create a throwaway account if you need an authenticated one.
- Confirms it now fails with the right error (denied, not "not found by accident").
- Confirms legitimate use still works — the regression half. Every tightening is a chance to lock out real users, and this is how you catch it before they do.
Report results as a table of expected vs actual. When something you were sure about comes back wrong, that is the check earning its keep — this pass will typically catch at least one fix that didn't work and one that broke something.
Clean up every artifact you create: test accounts, test records, generated credentials. Deleting a privileged test account matters as much as the audit — leaving one behind is its own vulnerability.
Phase 7 — Turn on enforcement last, and in the right order
Anything that rejects traffic — bot protection, attestation, WAF rules, stricter auth — must be deployed in observe-then-enforce order:
- Ship the client change that makes legitimate traffic identifiable.
- Deploy it.
- Wait and confirm the dashboard shows legitimate traffic being recognized.
- Only then turn on enforcement.
Enforcing before step 3 locks out every real user at once. Say this to the user plainly, because the console makes the toggle look harmless.
Related: after enforcement is on, your own scripted tests from Phase 6 will start failing — they're exactly what's being blocked. Register a debug/bypass token for testing, or note that future audits need one.
Reporting
Lead with what was actually exposed and what you did about it, in plain language, with severity that reflects real consequence rather than category. Then what needs the user (purchases, legal terms, credentials you shouldn't handle, decisions that affect their users). Then what you deliberately left.
Be straightforward about mistakes you find in your own earlier work — a bug you introduced is still a bug, and the user needs to trust the report more than they need you to look good.
What needs the user, not you
Be clear about these early rather than discovering them at the end:
- Purchases — domains, plans, paid tiers.
- Accepting terms of service on a third-party console.
- Typing secrets into forms. Ask them to paste the secret half themselves and send you only the public half.
- Credentials for services you can't reach, e.g. DNS at their registrar.
- Rotating anything they've already distributed to their users.
Reference files
references/injection.md— sinks, payloads, escaping and URL rulesreferences/authorization.md— ownership, get vs list, immutability, pathsreferences/headers.md— working CSP, per-service origins, common breakagereferences/launch-readiness.md— the polish checklist, with what to skipscripts/scan-client-secrets.sh— grep a build output for leaked secrets