Loading articles…
Security5 min read
A production security checklist for Next.js and Node.js: secrets, security headers and CSP, auth, server actions, validation and rate limiting.
Full-stack engineer, New Delhi
Next.js makes it easy to ship fast — and easy to ship something insecure without noticing, because server and client code live side by side. This is the checklist I run through before any client app goes live.
The biggest Next.js-specific mistake: leaking server-only data to the client.
NEXT_PUBLIC_ are bundled into the JavaScript every visitor downloads. Only public, non-secret values belong there.server-only package, so importing them into client code fails the build:// lib/db.ts
import "server-only"
export const db = createClient(process.env.DATABASE_URL!)Server Actions look like normal functions, but each one is an HTTP endpoint anyone can call. Every one needs:
"use server"
import { z } from "zod"
const Input = z.object({ title: z.string().min(1).max(200) })
export async function createPost(raw: unknown) {
const session = await auth() // 1. who is this?
if (!session) throw new Error("Unauthorized")
const data = Input.parse(raw) // 2. is the input valid?
if (!canCreatePost(session.user)) throw new Error("Forbidden") // 3. are they allowed?
return db.post.create({ data: { ...data, authorId: session.user.id } })
}Authentication, validation, authorisation — every time. Middleware is a convenience, not a security boundary: re-check authorisation where the data is read or written.
Set headers once in next.config:
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=()" },
]
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }]
},
}Then add a Content-Security-Policy. The strongest version uses a per-request nonce generated in middleware; the Next.js docs have a complete example. Start with Content-Security-Policy-Report-Only to see what would break before enforcing.
HttpOnly, Secure, SameSite=Lax, sensible expiry.Use one schema library (Zod, Valibot) at every boundary — forms, route handlers, server actions, webhooks, environment variables:
const Env = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
})
export const env = Env.parse(process.env) // crash at boot, not at 3 a.m.For webhooks (Stripe, Razorpay, GitHub), verify the signature before trusting the payload.
Serverless means you can't keep counters in memory. Use a shared store — Upstash Redis with @upstash/ratelimit, or your platform's firewall rules — and limit login, sign-up, OTP, password reset, contact forms and any AI or paid-API endpoint. Add a bot check like Cloudflare Turnstile on public forms.
npm ci / pnpm install --frozen-lockfile in CI.npm audit --omit=dev in CI.A final pass I do on every project:
gitleaks on the repo historyNone of this is glamorous, but it's the difference between an app that survives its first year and one that ends up as a breach notification. The underlying bug classes are covered in more depth in 8 web vulnerabilities every developer should know.
Security
Broken access control, injection, XSS, CSRF, SSRF, leaked secrets and more — how each attack works and how to fix it, with code examples.
Security
How password managers, passkeys and the right kind of 2FA protect you — plus a 30-minute plan to lock down your email, bank and work accounts.
Security
A plain-English, 20-point security checklist for small business websites: HTTPS, updates, backups, logins, headers and what to do if hacked.