Almost every hole in an AI-built Next.js app comes from a single confusion: not knowing which part of the code runs on the server and which part ends up in the browser. Next.js mixes both in the same project, and Cursor, v0 or Bolt cross the line without telling you. This guide explains that mental model in five minutes, goes through what AI tools get wrong in Next.js, leaves you a block of rules to paste into your project, a review prompt and four checks from outside. The point-by-point list of what to fix is in the Next.js checklist before launch; this is the why.
Part 1: where each thing runs
Five places, two worlds
In the App Router there are five places your code can live: Server Components (server only, never reach the browser), Client Components (marked with "use client", shipped to the browser whole), Route Handlers (app/api/.../route.ts, public URLs), Server Actions (functions with "use server" that the client invokes via POST) and the middleware (called proxy in the newest versions), which runs before every request. The last three plus Server Components are the server world; Client Components are the browser world. Everything you import into a Client Component, including constants and helper functions, travels into the bundle.
What reaches the browser
- Environment variables prefixed with
NEXT_PUBLIC_, inlined at build time. The rest exist only on the server. - Any data a Server Component passes as a prop to a Client Component. If you pass the whole
userobject, it travels with the password hash and the email. - The files in the
public/folder, as is. - The source maps, if you enable them with
productionBrowserSourceMaps.
Three mechanisms that prevent crossing the line
The server-only package: an import 'server-only' at the top of a file makes the build fail if anyone imports it from a Client Component. Use it in everything that touches the database or reads secrets. A data access layer: one module that centralises the session check and returns only the needed fields (a DTO), instead of querying the database from every page. And the rule that every entry point checks the session: Route Handlers, Server Actions and Server Components that read private data, each on its own. The middleware does not replace that check, as CVE-2025-29927 showed.
What Next.js does not do for you
It does not add security headers, does not rate-limit, does not validate Server Action input and does not protect routes just because they live in a folder called (admin). All of that is your code or configuration in next.config.
Part 2: what AI gets wrong in Next.js
- Everything with "use client". When a component needs state, the AI marks it as client and drags along the database query or the API key. Split: the server component fetches the data, the client one only renders.
- Protection only in the middleware. The middleware redirects if there is no cookie and the handler trusts it. Check the session in the handler too; that is what the middleware CVE taught, and versions before 15.2.3 or 14.2.25 remain vulnerable.
- Route Handlers and Server Actions without authentication or validation. A Server Action is a public endpoint even without a visible URL. Check who is calling and validate the body with a schema (zod or similar) before touching anything.
- Secrets with the public prefix.
NEXT_PUBLIC_OPENAI_KEYbecause the client component "needed it". The key goes into a Route Handler and the client calls the handler. - remotePatterns with a wildcard.
hostname: '**'in the image configuration turns your image optimizer into an open proxy for any URL. - Redirects with a parameter.
redirect(searchParams.next)without checking that the destination is an internal path: phishing with your domain. - fetch to user URLs. A handler that downloads whatever it is given can reach internal services of your network or your provider's.
- dangerouslySetInnerHTML with user content. Classic XSS; if you need HTML, sanitise it on the server.
- No headers, no limits. No CSP, no HSTS, no X-Frame-Options, and an endpoint that calls a paid model with no cap per user.
A block of rules for your project
Paste it into the rules file your tool reads (.cursor/rules, AGENTS.md or the equivalent) so it applies on every generation:
Security rules for this Next.js project:
- Components are Server Components unless they need state or browser events. A Client Component never imports modules that access the database or read process.env without the prefix.
- Every module that touches the database or secrets starts with import 'server-only'.
- Never use NEXT_PUBLIC_ for a secret key. Calls to services with a key go in a Route Handler or a Server Action.
- Every Route Handler and every Server Action checks the session and validates input with a schema before doing anything. The middleware does not count as authentication.
- Pass Client Components only the fields they need, never the whole user or row object.
- Redirects only accept internal paths starting with a single slash.
- images.remotePatterns lists specific domains, never a host wildcard.
- Do not use dangerouslySetInnerHTML with user content.
- next.config defines the security headers and does not enable productionBrowserSourceMaps.
A review prompt before you deploy
Review the security of this Next.js app and do not change anything yet. Answer with a list:
1. Client Components that import database modules, read secrets or receive objects with sensitive fields as props.
2. Route Handlers and Server Actions without a session check or without input validation.
3. Routes whose only protection is the middleware.
4. NEXT_PUBLIC_ variables containing secret keys.
5. Redirects that accept a user-supplied destination, fetches to user URLs and uses of dangerouslySetInnerHTML.
6. remotePatterns with a wildcard, productionBrowserSourceMaps enabled and security headers missing from next.config.
7. The Next.js version and whether it is older than 15.2.3 or 14.2.25.
For each point, propose the exact change and wait for my confirmation.
Check the result from the outside
Four commands against your domain. Headers and the middleware version:
curl -sI https://yourdomain.com | grep -iE "strict-transport|content-security|x-frame|x-content-type"
curl -s -o /dev/null -w "%{http_code}\n" -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" https://yourdomain.com/dashboard
If the second returns 200 on a route that should redirect to login, your version is vulnerable to the middleware CVE: update today. Then, source maps and the API routes the bundle reveals:
curl -s https://yourdomain.com | grep -oE '/_next/static/chunks/[^"]+\.js' | head -3
curl -s https://yourdomain.com/_next/static/chunks/main-app-XXXX.js | grep -oE '"/api/[^"]+"' | sort -u
Request each API route without a session: the right answer is a 401 or 403. Scanaris runs these checks and about forty more in 30 seconds: it detects that the app is Next.js and its version, tests the CVE header, looks for secrets in the chunks, checks whether source maps are published, reviews headers, CSP, cookies, CORS and exposed files, and gives you a fix prompt for each failure to paste into Cursor or v0. If you deploy on Vercel, the Vercel guide covers public previews and environment variables.
Frequently asked questions
Are Server Actions secure by default?
They are harder to discover than a Route Handler, but just as public: anyone with the action identifier can invoke it. Treat them as an endpoint: session and validation inside the function.
Is the middleware enough to protect the dashboard?
No. It is useful for a fast redirect, but the real check goes in every page, handler and action. CVE-2025-29927 allowed bypassing the middleware with one header; even on a patched version, the rule stays the same.
Can I keep using the Pages Router?
Yes, and the model is the same: getServerSideProps and pages/api are the server, the rest is client. Return from getServerSideProps only what the page needs, because everything you return is serialised into the HTML.