The checks Throttle Bird runs, written out so you can do them by hand.
The model said the app is done when the happy path rendered. Launch is a different question: what can a stranger learn from the URL, and what does the link look like when someone pastes it into a chat. This is the list we run by hand, and the list Throttle Bird approximates with a passive scan. Work top to bottom. The first items are the ones that cost money or leak data. The last items are the ones that make the launch look accidental.
Secrets and the browser
- Search the built client bundle, not just your editor.
NEXT_PUBLIC_,VITE_, and string literals in client components are already public. - Stripe in the browser is
pk_only.sk_,rk_, andwhsec_stay on the server. Roll anything that already shipped. - OpenAI and Anthropic keys (
sk-proj-,sk-ant-) are called from a route you control. The route knows who the user is. - No PEM private keys, no service-role JWTs, no
sb_secret_in client code. .envfiles with real values are not in git..env.examplehas empty placeholders.
Data rules
- Supabase: RLS is on for every table in
public. Policies name who the row belongs to.using (true)is a deliberate public table, or it is a bug. - The anon key may be in the client. Confirm with one
limit=1read that a private table does not answer it. - Firebase: realtime rules are not
.read: trueat the root. Firestore is notallow read, write: if true. The webapiKeycan stay; restrict it by referrer anyway. - Admin SDKs and service keys are imported only from server files.
The HTTP response
- The launch URL is HTTPS.
http://redirects to it. The certificate is not expiring this month. - These headers exist:
Content-Security-Policy(even a strict starter withframe-ancestors 'none'),Strict-Transport-Securityonce HTTPS is solid,X-Content-Type-Options: nosniff,Referrer-Policy, andPermissions-Policyturning off camera and mic you do not use. X-Frame-Options: DENYor CSPframe-ancestors. One of them is enough.- Cookies that are sessions are
Secure,HttpOnly, andSameSite. A client-side token inlocalStorageis a trade you should be able to explain.
Abuse, cheaply
- Any route that calls OpenAI or Anthropic, sends mail, or creates a Stripe object has a rate limit and returns 429 when it trips.
- Auth routes have a limit even if the happy path is "just a demo". Demos get scripted.
- File upload routes check type and size on the server. The client check is a convenience.
- Error responses do not include stack traces, SQL, or env var names.
The link you are about to share
<title>is the product, not "Create Next App" or "Home".- Meta description is one or two real sentences.
- Open Graph title, description, and a 1200×630 image. Paste the URL into a chat and look at the preview before your users do.
viewportiswidth=device-width, initial-scale=1. Then actually open it at a phone width. Sideways scroll is a launch bug.<html lang>is set.robots.txtexists and points at a sitemap. The sitemap lists the public pages, not/apiand not preview junk.- A staging deploy is
noindex. Production is not.
The boring operational bits
- You can name the person who can roll a key at 1 a.m. If that person is you, the dashboards are logged in somewhere that is not the laptop you are about to close.
- The domain you will say out loud is the canonical one. Apex or www, pick one, redirect the other.
- A 404 page exists and does not throw.
- If your app needs a real server to hold secrets, hosts like Railway let you set private variables in a dashboard (referral link: we may earn a cut, it costs you nothing).
- You have a way to hear about exceptions that is not "a user tweets".
- Analytics, if you add any, is something you can describe in a privacy note. A cookie banner for a tool you do not understand is not a privacy program.
How to use the list with an agent
Paste the section you are fixing, not the whole list. Agents given twenty tasks will half-finish all of them and mark the file done. One prompt, one change, then you look at the diff.
A scan is a way to regenerate the list from the live URL after the agent says it is finished. Run it against the deploy, not against localhost. Localhost cannot see the headers your host adds, and it cannot see the bundle your host actually serves.
None of this is a penetration test. It is the preflight that keeps the obvious stuff off the launch tweet. Do it before you announce the URL. Doing it the morning after also works. It is just a worse morning.