security//3 min read
A security checklist for early-stage web apps
You do not need a security team to avoid the common breaches. You need a handful of habits applied consistently: safe auth, server-side authorization, validated input, careful uploads, and secrets kept out of the repo.

In short
- Most early breaches come from missing authorization checks, not clever exploits.
- Check permissions on the server for every request; the UI hiding a button is not security.
- Validate all input at the edge of your system and treat uploaded files as hostile.
- Keep secrets out of the repository, rotate them, and know how you would respond to a leak.
Security advice tends to swing between the overwhelming and the vague. For a young product, a short list of habits covers most of the real risk. These follow the shape of the OWASP Top 10, trimmed to what matters first.
Authentication
- Hash passwords with a slow, salted algorithm such as bcrypt, scrypt or Argon2. Never store or log them in plain text.
- Keep sessions in cookies marked
HttpOnly,SecureandSameSite=Lax(orStrict), so scripts cannot read them and other sites cannot easily send them. - Rate-limit sign-in, sign-up and password reset, and avoid revealing whether an email address has an account.
- Offer two-factor authentication for admin accounts at the very least.
Authorization: the one most often missed
The most common serious bug in young apps is not a sophisticated attack. It is an endpoint that loads a record by its ID without checking that the current user may see it. Change /api/invoices/41 to /api/invoices/42 and you are reading someone else's invoice.
// Every query that loads user data is scoped to the caller.
const invoice = await db.query.invoices.findFirst({
where: and(eq(invoices.id, id), eq(invoices.teamId, session.teamId)),
});
if (!invoice) return notFound(); // same answer for "missing" and "not yours"Check permissions on the server for every request, in one shared place. Hiding a button in the interface is a convenience, not a control.
Input validation
Validate every request body, query string and webhook payload against a schema at the boundary, and reject what does not match. Use parameterised queries or a query builder so values are never concatenated into SQL. When rendering user content, let your framework escape it, and sanitise any rich text against an allow-list of tags and attributes.
File uploads
- Check the file's real type from its first bytes, not the extension or the browser's claim.
- Set a size limit and reject anything larger before reading it all into memory.
- Do not accept SVG or HTML uploads unless you sanitise them; both can carry scripts.
- Serve uploads with
X-Content-Type-Options: nosniff, ideally from a separate domain or with a restrictive Content Security Policy.
Security headers
A few headers close whole classes of attacks. In Next.js they can be set once:
// next.config.ts
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];
export default {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};Add a Content Security Policy next. Start in report-only mode, fix what it reports, then enforce it.
Secrets and dependencies
- Keep secrets in your platform's environment settings, never in the repository. Add
.env*to.gitignoreon day one. - Use separate keys for development and production, with the least access each needs.
- Turn on automated dependency alerts and update regularly; most real-world exploits target known, already-patched bugs.
- Know how you would rotate every key in an afternoon if one leaked.
Operations
- Automated daily backups, plus a restore you have actually tested.
- Logs for sign-ins, permission changes and admin actions, kept long enough to investigate.
- A public way to report vulnerabilities, such as a
security.txtfile and a monitored address.
Most breaches are not clever. They are a missing check that nobody noticed.
The checklist
- To do: Passwords hashed with bcrypt, scrypt or Argon2
- To do: Session cookies HttpOnly, Secure and SameSite
- To do: Rate limits on sign-in, sign-up and reset
- To do: Authorization checked on the server for every request
- To do: Every input validated against a schema
- To do: Uploads type-checked, size-limited and served safely
- To do: Security headers and a Content Security Policy
- To do: Secrets out of the repo, with a rotation plan
- To do: Dependency alerts on
- To do: Backups tested by restoring them


