Next.js App Router

290 fragments · Layer 3 Synthesized established · 290 evidence · updated 2025-01-31
↓ MD ↓ PDF

Summary

useSearchParams() must be wrapped in a Suspense boundary in every Next.js App Router project — this is the single most common build-breaking error across all eight projects in this corpus, and it surfaces at Vercel build time, not locally. Next.js 16 introduced a parallel breaking change: middleware.ts at the project root is deprecated in favor of src/proxy.ts with a proxy() export, and having both files present causes Vercel to fail looking for middleware.js.nft.json. The Edge Runtime is a persistent source of incompatibility — it rejects Node.js crypto, async_hooks/AsyncLocalStorage, and cookies from next/headers, all of which work fine in Node.js serverless functions. router.push() does not trigger middleware or establish server-side session cookies; use window.location.href when you need a full server round-trip after auth.


TL;DR

What we've learned
- useSearchParams() without a Suspense boundary breaks static generation at build time — not at runtime, not locally. Wrap it or add export const dynamic = 'force-dynamic'.
- Next.js 16 replaced middleware.ts with src/proxy.ts; leaving the old file in place causes Vercel build failures with a confusing middleware.js.nft.json ENOENT error.
- Edge Runtime silently rejects Node.js APIs (crypto, async_hooks, cookies from next/headers). The error only appears at build or cold-start, not in local dev with Node.js.
- router.push() uses the client-side cache and does not re-run middleware. After login or org creation, use window.location.href for a real server round-trip.
- Service worker stale-while-revalidate for HTML causes a frozen app after deployments — JS bundle hashes change but the cached HTML still references old chunks.

External insights

No external sources ingested yet for this topic.


Common Failure Modes

1. useSearchParams() breaks static generation at build time

Established failure mode across all eight projects. The build error looks like:

Error: useSearchParams() should be wrapped in a suspense boundary at page "/some-page".

This only surfaces during next build — local dev with next dev doesn't enforce the Suspense requirement because it always renders dynamically. The fix is either wrapping the component:

// ✅ Fix: wrap the component using useSearchParams in Suspense
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={null}>
      <ComponentThatUsesSearchParams />
    </Suspense>
  )
}

Or, for pages where you don't need static generation at all:

// ✅ Alternative: opt out of static generation entirely
export const dynamic = 'force-dynamic'

Observed in stride-v2, labelcheck, hazardos, contentcommand, and eydn. The useSearchParams in an OrgSwitcher component caused hydration errors in contentcommand specifically because the component was rendered in a shared layout without a boundary.
[1]


2. Next.js 16 middleware.ts → proxy.ts migration breaks Vercel builds

Consistent across projects. Next.js 16 deprecated middleware.ts at the project root in favor of src/proxy.ts with a named proxy() export. Having both files present causes Vercel to fail with:

Error: ENOENT: no such file or directory, open '.next/server/middleware.js.nft.json'

The migration requires three changes:

// src/proxy.ts (replaces middleware.ts)
export function proxy(request: Request) {
  // your auth/redirect logic
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

The config export must be defined directly in the file — re-exporting it from another module doesn't work in Next.js 16. Also: Next.js 16 expects proxy.ts at src/ level when using the src/ directory convention, not at the project root.

Observed in stride-v2, hazardos, orbitabm. Turbopack had a separate bug with middleware.js.nft.json generation that was fixed in Next.js 16.1.1 (build time dropped from ~96s to ~18s after upgrading).
[2]


3. Edge Runtime rejects Node.js APIs — silently in dev, loudly at build

The Edge Runtime does not support:
- crypto from Node.js (use Web Crypto API: crypto.subtle, or a pure-JS hash)
- async_hooks / AsyncLocalStorage (breaks monitoring middleware)
- cookies imported from next/headers
- eval() and dynamic require()
- Native modules (ssh2, ssh2-sftp-client, etc.)

The failure mode is that local dev runs on Node.js and everything works. The error appears at next build or on first cold-start in production:

Error: The edge runtime does not support Node.js 'crypto' module.
Error: Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') 
not allowed in Edge Runtime

For crypto, replace with Web Crypto:

// ❌ Breaks in Edge Runtime
import crypto from 'crypto'
const nonce = crypto.randomBytes(16).toString('hex')

// ✅ Works everywhere
const array = new Uint8Array(16)
crypto.getRandomValues(array)
const nonce = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('')

For native modules, add to serverExternalPackages in next.config.ts:

const nextConfig = {
  serverExternalPackages: ['ssh2-sftp-client', 'ssh2'],
}

Observed in asymxray (crypto/eval), stride-v2 (cookies import), labelcheck (CSP nonce generation), eydn (ssh2 for SFTP).
[3]


4. router.push() doesn't establish server-side session cookies

router.push() performs a client-side navigation using Next.js's prefetch cache. Middleware does not re-run. Server components do not re-fetch from the database. This causes two distinct failure modes:

After login: The session cookie isn't visible to server components until a full page reload. In client-brain, this caused a sign-in freeze where the user appeared logged in client-side but server components still returned unauthenticated state.

After org/client creation: The new org doesn't appear in the sidebar because the server component that renders the sidebar cached the old org list. In contentcommand, this required a hard navigation to force a server re-render.

// ❌ Client-side cache — middleware doesn't re-run
router.push('/dashboard')

// ✅ Full server round-trip — middleware runs, session established
window.location.href = '/dashboard'

Seen in two projects (client-brain and contentcommand).
[4]


5. Third-party scripts cause hydration error #418

Inline dangerouslySetInnerHTML scripts in <head>, and scripts loaded with strategy='beforeInteractive', cause React hydration error #418 because the server-rendered HTML includes the script but React's hydration pass doesn't expect it. Termly (consent management) and GTM were the specific offenders in eydn.

Error: Hydration failed because the initial UI does not match what was rendered on the server.

The fix is moving all third-party scripts to next/script with strategy='afterInteractive':

// ❌ Causes hydration error #418
<head>
  <script dangerouslySetInnerHTML={{ __html: termlyScript }} />
</head>

// ✅ afterInteractive runs after hydration completes
import Script from 'next/script'

<Script
  src="https://app.termly.io/embed.min.js"
  strategy="afterInteractive"
/>

The GTM <noscript> iframe also causes hydration mismatches between server and client — remove it entirely rather than conditionally rendering it.

Observed in eydn across three separate commits addressing the same root cause.
[5]


6. Service worker stale-while-revalidate for HTML freezes the app after deployments

When a service worker caches HTML with stale-while-revalidate, users get the old HTML after a deployment. The old HTML references JS bundle filenames from the previous build (e.g., /_next/static/chunks/app/page-a4b2c8e.js). Those files no longer exist on the CDN. The app loads but throws chunk-load errors and appears frozen.

Fix: use network-first for HTML, cache-first only for static assets:

// service-worker.js
workbox.routing.registerRoute(
  ({ request }) => request.destination === 'document',
  new workbox.strategies.NetworkFirst({ networkTimeoutSeconds: 5 })
)

workbox.routing.registerRoute(
  ({ request }) => ['script', 'style', 'image'].includes(request.destination),
  new workbox.strategies.CacheFirst()
)

Observed in asymxray.
[6]


7. File casing mismatches pass locally, fail on Linux deployments

Windows and macOS filesystems are case-insensitive. A component imported as @/components/Navigation resolves correctly even if the file is named navigation.tsx. Linux (Vercel's deployment environment) is case-sensitive and throws a module-not-found error.

Error: Cannot find module '@/components/Navigation'

This only surfaces at Vercel build time. The fix is ensuring import paths exactly match filenames. In labelcheck, the navigation component was imported with a capital N but the file used lowercase.
[7]


8. Hardcoded localhost:3000 breaks when dev server runs on another port

Any fetch() call with a hardcoded http://localhost:3000 fails when the dev server starts on port 3001, 3002, etc. (which happens automatically when 3000 is occupied). In asymxray, this caused Failed to fetch errors in the export functionality.

// ❌ Breaks on non-3000 ports
const res = await fetch('http://localhost:3000/api/export')

// ✅ Derive from request context in server components/routes
const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? 
  (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '')

For OAuth redirect URIs specifically, derive from request.nextUrl.origin to support any port dynamically.
[8]


9. ssr: false dynamic imports not allowed in Server Components (Turbopack)

Error: 'ssr: false' is not allowed with `next/dynamic` in Server Components

dynamic(() => import('./Component'), { ssr: false }) is a Client Component pattern. Using it in a Server Component fails under Turbopack. Move the dynamic import to a Client Component wrapper.

Observed in eydn when adding Google Places vendor cards.
[9]


10. Higher-order functions with 'use server' break Turbopack in Next.js 16

A utility function that wraps server actions (e.g., withOrgContext) should not have the 'use server' directive. In Next.js 16 with Turbopack, this causes a build error because the directive is only valid at the top of a file that directly exports server actions, not in utility wrappers.

Observed in stride-v2 when the withOrgContext utility was incorrectly marked.
[10]


What Works

Suspense + route groups for auth separation

Route groups ((auth), (dashboard)) cleanly separate authentication requirements and prevent infinite redirect loops. When the login page is inside (auth) and the dashboard is inside (dashboard), middleware can apply auth checks only to the dashboard group without accidentally redirecting the login page itself.

Validated in stride-v2 when fixing an admin login infinite redirect.
[11]


Scoped cache invalidation prevents cross-org data leakage

Cache keys should always include the organization ID. Unscoped invalidations (e.g., revalidatePath('/customers')) invalidate cache for all organizations, which causes one org's mutation to surface stale data for another org on the next request.

// ❌ Invalidates cache for all orgs
revalidatePath('/customers')

// ✅ Scoped to current org
revalidateTag(`customers-${organization.id}`)

Validated in hazardos. The same pattern applies to Redis cache keys in contentcommand.
[12]


ISR for sitemaps and blog pages

Sitemaps that regenerate on every request hit the database on every crawler visit. Adding ISR with a 1-hour revalidation serves the sitemap from edge cache for crawlers while keeping it reasonably fresh:

export const revalidate = 3600 // 1 hour

export async function GET() {
  // sitemap generation
}

For dynamic sitemaps that use environment variables at build time, add export const dynamic = 'force-dynamic' to prevent build-time failures when env vars are placeholders.

ISR on blog pages in eydn reduced TTFB from 1–1.6s to near-instant for cached hits.
[13]


Lazy loading heavy components for bundle size

next/dynamic with lazy loading for recharts, PDF viewers, and kanban boards consistently delivers meaningful bundle reductions:

const ReportViewer = dynamic(() => import('@/components/ReportViewer'), {
  loading: () => <Skeleton />,
})

Validated in hazardos across two separate optimization passes.
[14]


next build before tsc for type checking

Next.js App Router auto-generates RouteContext types during the build phase. Running tsc before next build fails because those types don't exist yet. The correct CI order is next build first, then tsc --noEmit for type checking.

Observed in eydn CI pipeline.
[15]


Database-backed admin auth over environment variable lists

Storing admin users in a database table (agency_users) and checking against it in an withAdminAuth helper is more reliable than maintaining an ADMIN_EMAILS environment variable. When admins are added to the database but not to the env var, they get 403 errors with no obvious cause.

Observed in asymxray where the env-var approach caused silent auth failures for legitimate admins.
[16]


Service layer extraction from API routes

Extracting business logic from API route files into service modules reduces route files from hundreds of lines to under 150, makes the logic testable in isolation, and eliminates duplicated auth/validation boilerplate. Measured results:

Validated across asymxray, labelcheck, hazardos.
[17]


Gotchas and Edge Cases

NEXT_PUBLIC_ env vars must be explicitly referenced in config files

Next.js inlines NEXT_PUBLIC_ variables at build time by scanning source files for references. If a variable is only accessed via a dynamic key (e.g., process.env[varName]) or only referenced in a config object that Next.js doesn't scan, the client-side value is undefined. Explicitly reference each variable in the source file where it's used.

Observed in orbitabm during Tailwind v4 migration.
[18]


globalThis.location?.search doesn't update during client-side navigation

window.location.search is a snapshot — it doesn't react to Next.js client-side route changes. Components that read URL parameters this way appear to work on initial load but show stale values after navigation. Use useSearchParams() from next/navigation instead.

Observed in eydn admin sidebar where the active tab highlight stopped updating after navigation.
[19]


process.env.CI detection creates production security holes

Using process.env.CI to skip validation in CI environments is dangerous because Vercel sets CI=true in production build environments. In asymxray, credential validation was being skipped in production deployments because the startup check used process.env.CI as the bypass condition. Use NODE_ENV === 'test' or a dedicated PLAYWRIGHT_TEST flag instead.
[20]


SSE connection loops from unstable callback references

Server-Sent Event connections in useRealTimeUpdates hooks re-establish on every render if the callback function is recreated each render (i.e., defined inline). This causes a connection loop. Fix: store the callback in a useRef so the SSE setup effect only runs once.

Observed in asymxray.
[21]


Competing router.push() calls create navigation race conditions

When both an AuthContext and a LoginForm component call router.push('/dashboard') after successful login, the two navigations race. Depending on timing, the user may land on an intermediate state or trigger a double-render. Centralize post-auth navigation to one location.

Observed in orbitabm.
[22]


router.replace vs router.push for URL parameter cleanup

After closing a modal that was opened via a URL parameter (e.g., ?modal=opportunity), router.push adds a new history entry. The user presses Back, the parameter reappears, and the modal reopens. Use router.replace to clean URL parameters without adding a history entry.

Observed in asymxray opportunity modal.
[23]


Supabase service account auth fails in cron/background contexts

Supabase's standard createClient() relies on cookies for session management. In cron job handlers, there's no request context and no cookies. Use createAdminClient() with the service role key for background/cron contexts.

Observed in asymxray detection system cron jobs.
[24]


Middleware role checks conflict with API endpoint authorization

When middleware checks admin role from environment variables and API endpoints check from the database, a user can be authorized at one layer and rejected at the other. In asymxray, removing the redundant middleware role check and relying solely on the API-level withAdminAuth resolved the inconsistency.
[25]


Missing tenant ID in middleware queries causes data leakage

In asymxray, CallRail API calls in middleware were fetching all companies because callrail_company_id was not included in the withClientAccess query filter. The result was cross-client data exposure. Every middleware query that touches multi-tenant data must include the tenant/org/client ID as a filter condition.
[26]


Where Docs Disagree With Practice

Turbopack is not production-ready in Next.js 16.0.x

The Next.js 16 announcement positioned Turbopack as the default bundler. In practice, Turbopack had a bug with middleware.js.nft.json generation that caused Vercel build failures. The workaround in asymxray was switching back to webpack for production builds. This was fixed in Next.js 16.1.1, which reduced build time from ~96s to ~18s.

// next.config.ts workaround (pre-16.1.1)
{
  "experimental": {
    "turbo": false
  }
}

[27]


strategy='beforeInteractive' does not prevent hydration conflicts — it causes them

The Next.js docs describe beforeInteractive as the strategy for scripts that must load before the page is interactive. In practice, beforeInteractive injects the script into <head> at build time, which creates a mismatch between server-rendered HTML and React's hydration pass. The result is hydration error #418. afterInteractive is the correct strategy for consent banners and analytics that must not block hydration.
[28]


Docs claim next/image wildcards are supported; explicit domains are safer

next/image supports remotePatterns with wildcard hostnames. Docs show this as a convenience feature. In practice, wildcard patterns create an open relay — any image from any subdomain of a whitelisted domain can be proxied through your Next.js image optimization endpoint. In eydn, this was tightened to explicit domain allowlists after a security review.
[29]


Third-party React components (Clerk, Stripe) cannot use SRI

Subresource Integrity requires a known hash at build time. Clerk and Stripe inject scripts dynamically at runtime with hashes that change between their deployments. SRI infrastructure can be built, but it cannot cover these components. CSP domain restrictions + HTTPS is the practical alternative.

Observed in labelcheck during a security hardening pass.
[30]


OAuth token refresh should not delete tokens on transient errors

A common pattern in OAuth implementations is to delete the stored token when a refresh fails, forcing re-auth. In practice, transient network errors (timeouts, 5xx from the OAuth provider) trigger this deletion and log users out unnecessarily. Only delete tokens on explicit revocation responses (HTTP 400 with invalid_grant).

Observed in asymxray OAuth stability improvements.
[31]


Tool and Version Notes



Sources

Synthesized from 290 fragments: git commits across 8 projects (asymxray, clientbrain, contentcommand, eydn, hazardos, labelcheck, orbitabm, stride-v2), 0 external sources, 0 post-mortems. Date range: unknown to unknown.

Sources

  1. Stride V2 2D319C0 Fix Admin Login Build Error Wrap Usesearchparams, Labelcheck 1439Ccc Fix Vercel Build By Wrapping Usesearchparams In Su, Hazardos 6862Cc4 Complete Console Statement Cleanup And Fix Deploym, Contentcommand E4D80D5 Ui Refresh Issues Hydration Error And Add Competit
  2. Stride V2 22C5Eb7 Remove Deprecated Middlewarets For Nextjs 16, Hazardos 6862Cc4 Complete Console Statement Cleanup And Fix Deploym, Orbitabm 324C8B7 Move Proxyts Into Src To Match Nextjs 16 File Conv, Asymxray 3C37E98 Update Dependencies And Remove Turbopack Workaroun
  3. Asymxray Bae0D74 Replace Crypto With Simple Hash For Edge Runtime C, Stride V2 E22Ebab Fix Middleware Remove Unused Cookies Import, Asymxray 297Ffb9 Remove Edge Runtime Incompatible Imports From Midd, Labelcheck E5032B2 Fix Csp Implementation For Edge Runtime Compatibil, Eydn App 07B4De2 Add Google Places Vendor Enrichment Cards And Fix
  4. Client Brain 75Ab8Fa Fix Sign In Freeze Use Full Page Reload After Succ, Contentcommand A16Bc7E Use Hard Navigation After Deleteregenerate To Bypa, Contentcommand Bc59A5F Orgclient Visibility After Creation
  5. Eydn App 9A701D3 Fix Hydration Crash Move Termlygtm To Nextscript, Eydn App C67Bcc7 Move Termly To Afterinteractive To Fix Hydration C, Eydn App 4A6D434 Remove Gtm Noscript Iframe Potential Hydration Mis
  6. Asymxray 4Fe211B Prevent Frozen App By Using Network First For Html
  7. Labelcheck B9A2F43 Fix Navigation Component Case Sensitivity For Linu
  8. Asymxray 9266792 Standardize Export Functionality And Fix Port Mism, Asymxray 62C1364 Support Dynamic Oauth Redirect Uri For Any Port
  9. Eydn App 07B4De2 Add Google Places Vendor Enrichment Cards And Fix
  10. Stride V2 647D22D Remove Use Server From With Org Contextts Utilitie
  11. Stride V2 C1Ff361 Fix Admin Login Route Use Route Groups To Separa
  12. Hazardos 4Ac68E3 Scope Cache Invalidations To Current Organization
  13. Eydn App 205Eb4E Cache Sitemap For 1 Hour Via Isr Revalidation, Eydn App 8786Cf3 Add Isr Caching To Blog Pages For Faster Load Time, Eydn App 44A1Bfb Fix Ci Build Mark Sitemap As Force Dynamic
  14. Hazardos 9Ce524D Add Lazy Loading For Heavy Components, Hazardos 5145524 Implement Bundle Optimization With Lazy Loading Fo
  15. Eydn App Cc90C43 Fix Ci Build Before Typecheck Upgrade To Node 22
  16. Asymxray 1970065 Use Consistent Admin Auth Across Monitoring Apis
  17. Asymxray 58643F7 Extract Service Layers From Large Api Routes, Labelcheck Fa1Abec Refactor Analyzetext Route To Use Service Layer, Labelcheck D69Da28 Add Getauthenticateduser Helper And Update Organiz
  18. Orbitabm 9A70Feb Resolve Client Side Env Validation Tailwind V4 Nav
  19. Eydn App 41C47Eb Fix Admin Sidebar Highlight Use Usesearchparams Fo
  20. Asymxray 8C6B5A2 Critical Security Issue With Startup Validation In
  21. Asymxray 1F68B3A Extend Threshold Settings To All Detectors And Fix
  22. Orbitabm E424204 Fix Auth Race Condition And Brand Auth Pages With
  23. Asymxray A89Ac76 Fix Opportunity Modal Close Issue
  24. Asymxray B623777 Detection System Cron Authentication And Progress
  25. Asymxray Ffa14Ce Remove Redundant Admin Role Check From Middleware
  26. Asymxray 8Af2Aa2 Add Callrail Companyid To Withclientaccess Queries
  27. Asymxray 4Bd1B7C Use Webpack Instead Of Turbopack For Production Bu, Asymxray 3C37E98 Update Dependencies And Remove Turbopack Workaroun
  28. Eydn App C67Bcc7 Move Termly To Afterinteractive To Fix Hydration C, Eydn App 9A701D3 Fix Hydration Crash Move Termlygtm To Nextscript
  29. Eydn App 60Becf5 Restrict Nextimage To Specific Allowed Domains
  30. Labelcheck 51A2C3E Add Sri Infrastructure And Documentation
  31. Asymxray 8Dd3E06 Remove Mock Data Fallbacks Improve Oauth Stability

Fragments (290)