Troubleshooting
Start every investigation in the browser console:
window.__ENVIf the object is there and has your key, the server side worked and the problem is in how the client reads it. If it's missing or empty, the problem is on the server.
env() returns undefined in a client component
Work through these in order:
- Is the script rendered?
PublicEnvScript(orEnvScript) must be in the root layout's<head>. With onlyPublicEnvProvider, there is nowindow.__ENV; read values withuseEnvContext()instead. - Is the variable set where the server runs? The process that runs
next startornode server.jsneeds it, not the machine that rannext build. Checkwindow.__ENV. - Does it have the prefix? Only
NEXT_PUBLIC_*variables are sent. UsemakeEnvPublic()for variables your platform names without it. - Does the code run before hydration?
instrumentation-client.tsand early third-party snippets run before the default script. Switch to<PublicEnvScript disableNextScript />. - Did you change the script strategy?
nextScriptPropswithoutstrategy: 'beforeInteractive', or withafterInteractive, runs the script too late.
The browser shows the build-time value
- A literal
process.env.NEXT_PUBLIC_Xin client code. Next.js replaces those at build time. Switch toenv('NEXT_PUBLIC_X'). - The route was prerendered.
EnvScriptandEnvProviderdon't opt into dynamic rendering. Callawait connection()before readingprocess.env, as shown in custom variables. - The server hasn't restarted. Environment variables are read when the process starts. Restart the container or roll the deployment. On Vercel, changes apply to new deployments only.
Hydration mismatch after adding next-dynenv
The server rendered one value and the browser another. Usual causes:
strategy: 'afterInteractive'innextScriptProps: client components hydrate beforewindow.__ENVexists.- An
EnvScriptobject that renames keys, such asNEXT_PUBLIC_API_URL: process.env.API_URL. The server readsprocess.env.NEXT_PUBLIC_API_URL, the browser reads the renamed key. UsemakeEnvPublic()inregister()instead. serverOnly()rendered in a client component. It returns the real value on the server and the fallback in the browser.
Environment variable 'X' is not public and cannot be accessed in the browser
Browser code asked for a key without the NEXT_PUBLIC_ prefix. If the value is safe to publish, expose it with makeEnvPublic() or rename it. If it's a secret, move the read to a server component, route handler, or server action. Don't paper over this one: on the server, that same call already rendered the secret into the HTML. See Security.
useEnvContext must be used within a EnvProvider or PublicEnvProvider
The component isn't inside a provider. Check that PublicEnvProvider wraps children in a layout above the component, and that the hook isn't called in the same layout that renders the provider.
makeEnvPublic() copies are missing
- It's in
next.configand you useoutput: 'standalone'. Standalone servers never evaluatenext.configat runtime. Move the call toregister()ininstrumentation.ts. - The source variable is unset. Look for
Skipped prefixing environment variablein the server logs. instrumentation.tsis in the wrong place. With asrc/directory it belongs atsrc/instrumentation.ts.
The script is blocked by Content Security Policy
The console shows a CSP violation for an inline script. Pass the request nonce through the nonce prop, for example { headerKey: 'x-nonce' }, and generate that header in proxy. With disableNextScript, the nonce prop is required. Script approach has the full setup.
Build fails with connection() accessed outside of <Suspense>
You have cacheComponents enabled. Wrap the part of the root layout that renders PublicEnvScript or PublicEnvProvider in <Suspense>. See dynamic rendering.
My pages are no longer static
That's expected. Runtime values need runtime rendering, so routes under PublicEnvScript or PublicEnvProvider render per request. Keep static sections under a separate root layout if they don't need runtime config.
TypeScript wants a string, gets string | undefined
Pick the call that matches your intent:
const optional = env('NEXT_PUBLIC_API_URL') // string | undefined
const withDefault = env('NEXT_PUBLIC_API_URL', 'http://localhost:8080') // string
const required = requireEnv('NEXT_PUBLIC_API_URL') // string, throws if unsetStill stuck?
Issues are disabled on the repository. If you've found a bug, a pull request with a failing test or a fix is the fastest path.