Skip to content

Before You Deploy

Three things apply regardless of how you deploy.

Every project has a web/.env.example listing every variable the app needs. A few are browser-facing (their names start with NEXT_PUBLIC_) and get built into the app; the rest are server-side (API URLs, secrets) and are read by the running server. You don’t have to sort them yourself. One routine covers both.

Fill in one file, build with it, then put that file on your server. On your build machine, inside web/:

  1. Install: npm ci. This also creates web/.env.local from .env.example if it isn’t there yet.
  2. Fill it in: open web/.env.local and set the real production value for every variable, replacing the localhost defaults.
  3. Build: npm run build. The build reads .env.local, so the browser-facing values are baked into the app.
  4. Copy it to your server: save web/.env.local as .env in the deployment folder next to server.js (each platform guide shows where). The standalone server reads a .env from its own folder automatically on startup, so there’s nothing else to wire up.

Both .env.local and .env are gitignored, so secrets are never committed. To change a value later, see Changing an environment variable: a browser-facing value needs a rebuild; a server-side value means editing the server’s .env and restarting.

Docker works a little differently: its image is built in isolation and can’t read your env file, so browser-facing values are passed on the build command (--build-arg) and server-side ones via --env-file, which reads web/.env.local from the host at run time. See the Docker guide.

You need a reverse proxy in front of your app for TLS. The proxy must pass three headers so the app knows what the original request looked like:

HeaderWhat it tells your app
HostThe hostname the user typed (e.g. app.example.com).
X-Forwarded-ProtoWhether the original request was http or https. The app uses this to build correct absolute URLs and decide whether to mark cookies as Secure.
X-Forwarded-ForThe real client IP address.

Each deployment guide shows how to configure these headers for its platform.

This matters only when the browser talks to a separate auth host directly: external-provider (SSO) sign-in, or an auth API the browser calls cross-origin. If your sign-in posts to a same-origin path that your app rewrites to the auth API (the common username/password setup), the session cookie is first-party to your app and none of this applies. See Deploying a Separate Auth API to work out which case you’re in.

When the auth service is a separate host the browser reaches directly, two cookie properties both have to be right.

1. Same registrable domain: controls whether the cookie is sent at all. The frontend and the auth endpoint must share the same registrable domain (the part you buy from a registrar). For both app.example.com and auth.example.com, that’s example.com. Browsers only send auth cookies on requests that share this domain. Ports don’t factor into the comparison.

FrontendAuth endpointSame registrable domain?
app.example.comauth.example.comYes, both example.com
app.example.comauth.example.netNo, login will fail
localhost:3000localhost:10010Yes, ports don’t affect same-site

If you can’t put both services under one domain, use the reverse proxy to expose the auth endpoint under a path on your main domain (e.g. https://app.example.com/auth/... proxying through to the real auth service). The browser then only ever sees one domain.

2. The Domain attribute: controls which hosts receive the cookie. Sharing the registrable domain is necessary but not sufficient. When the auth service sets the cookie, its Domain attribute decides which hosts get it back:

  • No Domain (host-only): sent only to the exact host that set it (e.g. auth.example.com). Your app on app.example.com never receives it, so its session check sees no cookie and bounces the user back to login.
  • Domain=example.com: sent to all subdomains, so both hosts get it. This is what you want, alongside Secure, HttpOnly, and SameSite.

This hides in local testing: a host-only cookie set by localhost is still sent to localhost on any port, so a missing Domain works locally and only breaks once the two are on different subdomains in production. Make the cookie’s Domain an environment setting: unset locally, the shared parent domain in production. (Don’t set Domain=localhost; several browsers reject it and silently drop the cookie.)

If your project doesn’t use authentication, or your sign-in is a same-origin username/password form, skip this section.