Every week we see the same pattern: a site goes live with a public form, an API key pasted straight into the code, and zero server-side validation. None of this is exotic — it's the same 20 points, over and over.
What's almost always missing
It's not a budget problem or "we ran out of time." It's that nobody sat down to run the full list before saying "okay, it's live." So we turned it into a standing checklist — the same one we now run in every office before calling any site launched.
"Hiding an API key isn't optional — it's the first item on the list, and the most ignored."
If your site is a static landing page with no backend, the "Authentication & data access" block doesn't apply yet — but go through the rest anyway. The moment you add a form with a database, a login, or a public webhook, the full list comes into play. Here are all 20, with the concrete reason each one matters.
Secrets & keys
- Hide API keys — never hardcoded in the frontend or the repo. Anyone can open the browser devtools and read the source; a Stripe or OpenAI key sitting there is exposed to the world, and bills come out under your name.
- Remove secrets from Git — check the history, rotate any key that's ever leaked. Deleting the file isn't enough: the commit history keeps it forever unless you rewrite history or rotate the key. Bots scan GitHub for exactly this.
- Public vs. private database keys — only the "anon/public" key goes in the client (e.g. Supabase/Firebase); the "service_role"/secret key never leaves the backend. Mixing these up is the #1 mistake we see, and it leaves the entire database editable by any visitor.
Authentication & data access
- Row-Level Security (RLS) enabled in the database, not just frontend checks. A "only the owner can see this" check in React protects nothing — anyone can call the API directly. RLS enforces it in the database itself.
- Encryption for sensitive data — HTTPS in transit, encryption at rest where it applies. Without this, customer data travels or sits readable; a stolen backup or a compromised public network exposes everything.
- Server-side authentication enforced on every protected endpoint/route, not just a hidden button. Hiding an "admin" button in the frontend blocks nothing — anyone can call that route directly by URL.
- Record access restricted by owner/tenant — one user shouldn't be able to request another's ID. Changing a number in the URL (
?id=124→125) shouldn't show you someone else's data; it's one of the most common and most expensive bugs in multi-user apps. - Block field manipulation (mass assignment) — the client shouldn't be able to set role/price/admin via payload. If your API accepts any field in the body, a user can send
"role":"admin"and the backend just accepts it. - Cookies with HttpOnly, Secure, SameSite flags. Without these flags, an injected script (XSS) can steal any user's session by reading their cookie directly.
- Hashed passwords (bcrypt/argon2), never plaintext or reversibly encrypted. If your database ever leaks — it happens more than you'd think — plaintext passwords expose your users' accounts on other sites too, since almost nobody uses a different password per site.
- Login attempt limits (rate-limiting / lockout after repeated failures). Without this, a bot can try thousands of passwords a minute against a single account until one works.
"Hiding an API key isn't optional — it's the first item on the list, and the most ignored."
Input & content
- Always use parameterized queries (never concatenate SQL). Dropping raw user input straight into a query is the entry point for the oldest — and still the most common — injection attack on the internet.
- Validate every input server-side, don't rely on frontend validation alone. A JavaScript form check is bypassed with a direct API call; the server is the only place validation actually counts.
- Escape user-generated content before rendering it (prevents XSS). A comment or username with code inside it, if not escaped, runs in the browser of anyone who views it.
- Restrict file uploads (type, size, no executables, served without execute permission). A form that accepts "any file" can end up serving an executable script disguised as an image from your own domain.
Network & abuse
- Anti-bot protection on public forms (captcha/rate-limiting). Without this, a contact form becomes a free spam generator, and your real-lead inbox fills up with junk.
- Rate limits/quotas on any public API. An API with no limit is an open invitation for someone to overload it — on purpose or not — and take the service down for everyone else.
- Security headers: CSP, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy. Each one closes a different door — from your site being embedded in someone else's iframe (clickjacking) to an old browser misreading a file type.
- Force HTTPS everywhere (redirect + HSTS). Without HSTS, a user's first visit to your site can go over unencrypted HTTP before the redirect kicks in — a real window for intercepting the connection.
This is the same list we run internally before calling any site launched — point by point, without skipping any of them just because they "probably don't apply."
