# Final App Push

> Pre-launch hardening pass for an app that was built fast (vibe-coded) and is about to go public — or is already public and was never audited. Hunts for secrets compiled into the client bundle, stored XSS, missing server-side authorization, and the launch-readiness gaps (404, headers, sitemap, privacy policy, meta tags), then PROVES each fix by attacking the live system. Use this whenever the user says they're about to launch, publish, deploy, go live, or "make it real"; whenever they ask to "make it secure", "check for vulnerabilities", "do a security review", or "harden" an app; whenever they paste a pre-launch checklist from a video, thread, or blog post and ask you to implement it; and whenever they mention protecting paid or gated content, user accounts, admin dashboards, or personal data. Also use it proactively before any first deploy of an app that handles logins or user data.

- Skill: `nt11111/final-app-push` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add nt11111/final-app-push`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nt11111/final-app-push/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: nt11111 (https://skillmd.com/u/nt11111)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nt11111/final-app-push

---


# 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:

```bash
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 `secret` are 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.
- **`escapeHtml` that doesn't escape quotes.** The common
  `textContent`-then-`innerHTML` trick 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, so
  `java\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 `get` from `list`,
  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'`, not `DENY`, 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:

1. **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.
2. **Confirms it now fails** with the right error (denied, not "not found by
   accident").
3. **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:

1. Ship the client change that makes legitimate traffic identifiable.
2. Deploy it.
3. **Wait and confirm the dashboard shows legitimate traffic being recognized.**
4. 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 rules
- `references/authorization.md` — ownership, get vs list, immutability, paths
- `references/headers.md` — working CSP, per-service origins, common breakage
- `references/launch-readiness.md` — the polish checklist, with what to skip
- `scripts/scan-client-secrets.sh` — grep a build output for leaked secrets

