Next.js — Overview¶
OWNER: floris, floor
ALSO_USED_BY: urszula, maxim, alexander
LAST_VERIFIED: 2026-03-26
GE_STACK_VERSION: next@15.5.x (pinned), react@19.x, react-dom@19.x
Overview¶
Next.js App Router is GE's primary web framework for all client SaaS projects.
All new projects use App Router exclusively. Pages Router is legacy — never start new work with it.
GE pins to Next.js 15.5.x stable. Next.js 16 upgrade is tracked by Joshua (Innovation).
GE Version Pinning¶
CHECK: package.json has exact minor version pinned
IF: version is ^15 or ~15 (floating)
THEN: pin to 15.5.x — GE does not float majors or minors
CHECK: next.config.ts uses TypeScript config (not .js or .mjs)
CHECK: turbopack is enabled for dev (next dev --turbopack)
CHECK: output: "standalone" is set for Docker/k3s deployment
When to Use What¶
Rendering Strategy Decision¶
IF: page is public, content rarely changes (marketing, docs)
THEN: Static Generation (SSG) with ISR revalidation
IF: page is authenticated, shows user-specific data
THEN: Server-Side Rendering (SSR) with no-cache
IF: page has mix of static shell + dynamic personalized content
THEN: Partial Prerendering (PPR) — static shell + streamed dynamic holes
IF: page is purely interactive (dashboard widgets, real-time)
THEN: Server Component wrapper + Client Component leaves
Feature Selection¶
IF: form submission or data mutation
THEN: Server Actions (not API routes)
READ_ALSO: wiki/docs/stack/nextjs/data-fetching.md
IF: auth check before page load
THEN: Middleware with JWT verification
READ_ALSO: wiki/docs/stack/nextjs/middleware.md
IF: SEO-critical page
THEN: generateMetadata() in layout/page, not
IF: complex multi-panel UI (modal over page, sidebar + main)
THEN: Parallel routes + intercepting routes
READ_ALSO: wiki/docs/stack/nextjs/app-router.md
GE Project Structure Convention¶
src/
├── app/ # Routes only — no business logic here
│ ├── (public)/ # Route group: unauthenticated pages
│ ├── (dashboard)/ # Route group: authenticated pages
│ ├── api/ # API routes (minimal — prefer Server Actions)
│ ├── layout.tsx # Root layout (required)
│ ├── not-found.tsx # Global 404
│ └── error.tsx # Global error boundary
├── components/
│ ├── ui/ # shadcn/ui primitives (auto-generated)
│ └── {feature}/ # Feature-specific components
├── lib/
│ ├── actions/ # Server Actions
│ ├── db/ # Drizzle schema + queries
│ ├── services/ # Business logic (pure functions)
│ └── utils/ # Shared utilities
├── hooks/ # Client-side React hooks
├── types/ # TypeScript type definitions
└── styles/ # Global CSS (Tailwind entry)
ANTI_PATTERN: putting business logic in app/ route files
FIX: extract to lib/services/ — route files import and call
ANTI_PATTERN: creating components/ inside app/ route folders
FIX: use src/components/{feature}/ — colocated components cause import confusion
GE-Specific Conventions¶
- TypeScript strict mode always (
"strict": truein tsconfig) - All styling via Tailwind CSS + shadcn/ui — no CSS modules, no styled-components
- Drizzle ORM for database access — never raw SQL in components
- Server Actions for mutations — API routes only for webhooks/external integrations
output: "standalone"in next.config for Docker builds- Environment variables:
NEXT_PUBLIC_prefix only for client-safe values - All data EU-hosted — no Vercel Analytics, no US-based CDN origins
- Images served via
next/imagewith explicit width/height — no unoptimized - Error boundaries at route segment level (
error.tsx), not global try-catch - Loading states via
loading.tsx+ Suspense boundaries, not client spinners
next.config.ts Baseline¶
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
typescript: {
ignoreBuildErrors: false, // GE: NEVER set to true
},
eslint: {
ignoreDuringBuilds: false, // GE: NEVER set to true
},
images: {
remotePatterns: [
// Add per-project CDN domains here
],
formats: ["image/avif", "image/webp"],
},
experimental: {
ppr: "incremental", // Partial Prerendering per-route opt-in
typedRoutes: true,
},
headers: async () => [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
],
},
],
};
export default nextConfig;
CHECK: ignoreBuildErrors is false
CHECK: ignoreDuringBuilds is false
CHECK: output is "standalone"
CHECK: security headers are present
Upgrade Path: Next.js 15 → 16¶
TOOL: pnpm dlx @next/codemod@canary upgrade latest
KEY_CHANGES:
- Turbopack becomes default bundler (already used in GE dev)
- middleware.ts renamed to proxy.ts (runs Node.js, not Edge)
- PPR stabilized via cacheComponents config (replaces experimental.ppr)
- React Compiler support (stable)
- useFormState replaced by useActionState
IF: upgrading to Next.js 16
THEN: coordinate with Joshua (Innovation) + full team discussion
THEN: update GE_STACK_VERSION in ALL wiki pages under wiki/docs/stack/nextjs/
Security¶
CHECK: patched against CVE-2025-29927 (middleware bypass via x-middleware-subrequest)
IF: self-hosted (GE always is — k3s, not Vercel)
THEN: must be on Next.js 15.2.3+ (GE pins 15.5.x — safe)
CHECK: no secrets in NEXT_PUBLIC_ env vars
CHECK: Server Actions validate input with Zod (.issues not .errors — Zod v4)
CHECK: CSRF protection on Server Actions (Next.js built-in, verify not disabled)
Cross-References¶
READ_ALSO: wiki/docs/stack/nextjs/app-router.md
READ_ALSO: wiki/docs/stack/nextjs/server-components.md
READ_ALSO: wiki/docs/stack/nextjs/data-fetching.md
READ_ALSO: wiki/docs/stack/nextjs/middleware.md
READ_ALSO: wiki/docs/stack/nextjs/performance.md
READ_ALSO: wiki/docs/stack/nextjs/pitfalls.md
READ_ALSO: wiki/docs/stack/nextjs/checklist.md