Next.js code review standards
These Next.js code review standards cover App Router mistakes that compile, type-check, and work on a developer machine: Server Actions that skip authorization, secrets and server data that reach the browser, per-user data in shared caches, and data loading that serializes requests or blocks streaming. Each agent checks where a value crosses the server and client boundary, what a cache key contains, and where a Suspense boundary sits, none of which default lint rules track.
Next.js agents
- Security
Authorize and Validate Every Next.js Server Action
Flags Next.js Server Actions that skip session checks, trust client-supplied record IDs, or write unvalidated input to the database.
- Security & privacy
Keep Secrets and Server-Only Code Out of Next.js Client Bundles
Catches secrets behind NEXT_PUBLIC_, server modules imported into Client Components, and full database records or tokens passed to the browser.
- Security & privacy
Keep Per-User Data Out of Shared Caches in Next.js
Finds per-user data kept in module state, under cache keys that omit the user, or in public CDN responses, plus mutations that skip revalidation.
- Performance
Avoid Request Waterfalls in Next.js Server Components
Flags Next.js Server Components, pages, and layouts that await independent requests one at a time instead of starting them in parallel.
- Performance
Stream Slow Data in Next.js Instead of Blocking the Route
Flags slow awaits that hold back an entire Next.js layout or page, and Suspense boundaries placed where they cannot stream anything.
Also recommended for Next.js
Agents that apply to any codebase.
- Frontend
Enforce React and Next.js Production Guardrails
- Security
Flag Security Vulnerabilities
- API safety
Require API Auth, Validation, and Tenant Scoping
- Security & privacy
Prevent Sensitive Data in Logs
- Database
Prevent Database Query Performance Regressions
- Accessibility
Enforce Accessibility Standards (WCAG 2.1 AA)
- Testing
Enforce Pragmatic Test Coverage
Copy all 12 agents
Every agent on this page in one file, ready for your AI coding tool or for cubic.yaml.
AGENTS.md
Add these sections to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
# Next.js code review standards
Follow these standards when writing or changing code in this repository. Code review flags changes that break them.
## Authorize and Validate Every Next.js Server Action
Applies to `**/*.ts`, `**/*.tsx`.
Review Next.js Server Actions ("use server" functions, inline or in action files) for missing authentication, authorization, and input validation.
Flag changes that:
- Add or modify a Server Action that reads or writes data without verifying the session inside the action or in a server-side helper it calls.
- Rely on an auth check in the page or layout that renders the form; the action is a separate POST endpoint and that check does not protect it.
- Load, update, or delete a record by an ID taken from arguments, bound arguments (action.bind(null, id)), or form fields without checking that the current user owns it or belongs to its tenant.
- Take userId, orgId, role, price, or similar authority fields from the client instead of deriving them from the session or the database.
- Use arguments or FormData values without runtime validation inside the action, such as formData.get('email') as string or relying on TypeScript parameter types, which are erased at runtime.
- Pass Object.fromEntries(formData) or another unvalidated object straight into an ORM create or update, letting callers set fields such as role or ownerId.
- Check only that a user is signed in before admin, billing, or member-management changes, with no role or permission check.
Do not flag:
- Actions that delegate to a server-only data access function that performs the session, ownership, and validation checks, when those checks are visible in the called code.
- Actions built with a shared authenticated-action wrapper that enforces the session and parses input with a schema.
- Intentionally public actions, such as a newsletter signup or contact form, that validate input and do not touch user-owned records.
## Keep Secrets and Server-Only Code Out of Next.js Client Bundles
Applies to `**/*.ts`, `**/*.tsx`, `next.config.*`, `**/.env*`.
Review Next.js code for secrets, server-only modules, and private data that reach the browser.
Flag changes that:
- Put a secret behind a NEXT_PUBLIC_ variable (secret API keys, service-role or admin keys, signing secrets, database URLs), or rename a server variable to NEXT_PUBLIC_ so a Client Component can read it.
- Add secrets to the env key in next.config, which inlines values into client bundles regardless of prefix.
- Import a module that reads private env vars, creates a database or ORM client, or calls internal services into a "use client" file or anything it imports.
- Add a data access, auth, or secrets module that Client Components could import, without import "server-only" at the top.
- Pass full database records, ORM models, session objects, or access and refresh tokens as props from a Server Component to a Client Component that renders a few fields.
- Return raw database records or internal objects from a Server Action instead of only the fields the UI needs.
- Type Client Component props as whole server models (user: User from the ORM), which invites callers to pass private fields.
Do not flag:
- Values designed to be public behind NEXT_PUBLIC_, such as publishable or anon keys meant for browsers, analytics IDs, and public URLs.
- Private env vars read in Server Components, Route Handlers, "use server" files, or server-only modules.
- Props built from an explicit field selection or DTO mapping, even when the object is large.
## Keep Per-User Data Out of Shared Caches in Next.js
Applies to `**/*.ts`, `**/*.tsx`, `next.config.*`.
Review Next.js server code, caches, and mutations for per-user data served from shared caches and for cached routes left stale after writes.
Flag changes that:
- Store request-scoped data (current user, session, tenant, locale, auth token) in module-level variables, singletons, or Maps in server code, which every request on that server instance shares.
- Cache per-user or per-tenant results with unstable_cache, a Redis or KV helper, or a similar key-based cache when the user or tenant ID is missing from the key; unstable_cache does not add closure variables to its key.
- Set Cache-Control: public or s-maxage on responses that depend on cookies, the session, or the Authorization header, whether in Route Handlers, proxy or middleware, or next.config headers().
- Mutate data in a Server Action or Route Handler without calling revalidatePath, updateTag, revalidateTag, or the project's invalidation helper for the cached routes that display that data.
- Call revalidateTag(tag, 'max') in a Server Action whose user expects to see the change at once; 'max' serves stale data while revalidating, and updateTag expires it immediately.
- Revalidate one literal path, such as revalidatePath('/posts'), when the same data also appears on other cached routes such as detail pages or dashboards; a literal path refreshes only that page.
- Call revalidatePath or revalidateTag after redirect(); redirect throws, so the revalidation never runs.
Do not flag:
- React cache() and other per-request memoization, which is scoped to one request.
- Cached functions whose arguments or key parts include the user or tenant ID.
- Public data that is identical for every visitor, such as marketing pages, docs, or a product catalog.
- Writes to data that no cached or statically rendered route displays.
## Avoid Request Waterfalls in Next.js Server Components
Applies to `**/*.ts`, `**/*.tsx`.
Review Next.js Server Components, pages, layouts, generateMetadata, Route Handlers, and data helpers for independent requests that run one after another.
Flag changes that:
- Await two or more independent calls in sequence (fetch, ORM or SQL queries, SDK calls) when the later call does not use the earlier result; start them together and await Promise.all, or Promise.allSettled when partial failure is acceptable.
- Write Promise.all([await a(), await b()]) or await each promise before collecting them, which still runs the calls one at a time.
- Await inside a for or for...of loop when iterations do not depend on each other, instead of mapping to promises and awaiting them together or using one batched query.
- Make a parent Server Component await data that only a child uses before rendering the child, so the child's own requests start late; let the child fetch its data, or start the request in the parent and pass the unawaited promise down.
- Move data needed on first render into a Client Component useEffect when a Server Component could load it, adding a render, hydrate, then fetch round trip on every visit.
Do not flag:
- Calls that need an earlier result, such as loading a team by user.teamId, or an auth check that must pass before data loads.
- Writes that must happen in order, transactions, or loops that deliberately throttle a rate-limited API.
- Awaiting params, searchParams, cookies(), or headers() before the data calls.
- A layout and its page each awaiting their own data; Next.js renders them in parallel.
## Stream Slow Data in Next.js Instead of Blocking the Route
Applies to `**/*.tsx`.
Review Next.js layouts, pages, and async Server Components for slow data that holds back the whole response instead of streaming behind a Suspense boundary.
Flag changes that:
- Await slow or secondary data (recommendations, activity feeds, analytics, third-party APIs, large aggregates) at the top of a page before returning any JSX, when neither a loading.tsx nor a Suspense boundary covers it.
- Await request-time data at the top of a layout.tsx; it delays every page under that layout, and the same segment's loading.tsx does not cover the layout itself.
- Await data in a parent and pass the result into a component wrapped in <Suspense>, so the boundary sits below the await and its fallback never shows; move the await into the wrapped component, or pass the unawaited promise to a Client Component that reads it with use().
- Put a fast, critical request and a slow, secondary one in the same top-level Promise.all, so the critical content waits for the slowest call.
- Place several independent slow sections behind one shared Suspense boundary, so each waits for the slowest instead of streaming in as it resolves.
- Remove a loading.tsx or Suspense boundary that covered slow data without replacing it.
Do not flag:
- Fast awaits the page cannot render without, such as the session, the primary record, or a check that calls notFound() or redirect(); keeping these before any Suspense boundary preserves a real HTTP status.
- Data that is cached or prerendered at build or revalidation time instead of fetched per request.
- Pages where the slow data is the primary content and a loading.tsx already shows a meaningful skeleton.
## Enforce React and Next.js Production Guardrails
Review React and Next.js changes for production-safe component boundaries, routing, and rendering behavior.
Flag changes that:
- Move pages or layouts to "use client" without a clear need for browser APIs, hooks, or event handlers.
- Put protected route checks only in middleware when the page or layout also needs server-side authorization.
- Use direct DOM APIs in React components instead of refs or framework APIs.
- Introduce hydration-prone code, such as reading browser-only state during server render.
- Add raw inline styles or hard-coded visual values when project design tokens or shared components exist.
## Flag Security Vulnerabilities
Scan for security flaws: hardcoded secrets, SQL injection, XSS, weak auth, exposed data, poor crypto, unsafe deserialization, insecure HTTP calls in production, and more. Ensure all network requests use HTTPS/TLS. Identify attack vectors and security violations to maintain robust defenses.
## Require API Auth, Validation, and Tenant Scoping
Review API handlers and controller endpoints for missing authentication, authorization, tenant scoping, and request validation.
Flag new or modified endpoints that:
- Accept request bodies without runtime validation, such as Zod schemas, DTOs, or framework-native validators.
- Trust user-supplied identifiers for tenant, organization, or account access instead of deriving access from the authenticated session.
- Return sensitive internal errors or use inconsistent HTTP status codes for 400, 401, 403, 404, and 500 cases.
- Mix reads and writes in a way that makes side effects hard to reason about.
- Add public or high-volume endpoints without rate limiting or abuse controls.
## Prevent Sensitive Data in Logs
Logging, error reporting, analytics events, and audit trails must not expose sensitive data. Flag code that logs PII, emails, passwords, API keys, auth tokens, session identifiers, payment data, or raw request/response bodies without redaction. Error serialization must preserve enough context to debug while sanitizing secrets and user data.
## Prevent Database Query Performance Regressions
Flag database access patterns that can degrade production performance, including N+1 queries, unbounded list endpoints, missing pagination, full-table scans, missing indexes on selective filters, per-row writes in loops, and queries that load more columns or rows than needed. Prefer bounded reads, batching, selective indexes, and query shapes that scale with table size.
## Enforce Accessibility Standards (WCAG 2.1 AA)
Check for proper alt text on images, logical heading hierarchy (one h1, no skipped levels), ARIA attributes on interactive elements, keyboard navigation support, and screen reader compatibility. Ensure all UI components meet WCAG 2.1 AA standards.
## Enforce Pragmatic Test Coverage
Be reasonable when evaluating tests. Flag changes when important behavior is untested, but do not require tests for every export, type, config, metadata file, or mechanical change.
Flag PRs that:
- Change core business logic without covering the main success and failure paths.
- Add UI behavior without testing the user-visible interaction or state change.
- Add tests that only assert rendering, existence, snapshots, or that a function returns any value.
- Skip, mute, or weaken existing tests without a clear reason.
- Mock so much of the system that the test no longer validates the behavior the PR claims to change.
Questions and answers
- What should a Next.js code review checklist include?
- Start with the server and client boundary: every Server Action should verify the session and ownership of the record it changes, "use client" files should not import modules that hold secrets or database clients, and Client Components should receive only the fields they render. Then check caching: per-user data must not live in module-level state or under a cache key that omits the user, and every mutation must revalidate the cached routes that display the data. Finally, look for sequential awaits of independent requests and for slow awaits that block a layout or page with no Suspense boundary.
- Are Next.js Server Actions secure by default?
- Next.js adds some built-in protection: action IDs are encrypted and periodically regenerated between builds, unused actions are removed from the client bundle, actions accept only POST requests, and the Origin header is compared with the host to block most cross-site requests. None of that checks who the caller is or what they may change. The Next.js docs say to treat every Server Action as reachable by a direct POST request with arbitrary arguments, so each one must authenticate the user, authorize access to the specific record, and validate its input with a runtime schema.
- How do I make AI coding tools follow these Next.js standards?
- Paste the AGENTS.md export into AGENTS.md at the root of your repository. Codex, Cursor, and other AI coding tools read that file. Claude Code reads CLAUDE.md, and GitHub Copilot reads files in .github/instructions, so there is an export for each.
- How does cubic enforce these Next.js standards?
- cubic runs each agent on every pull request, up to your plan’s per-repository agent limit, and leaves an inline comment, naming the agent, when a change breaks the standard. Add an agent from its page, or commit the cubic.yaml export to your repository.