The check added in 1ee4b5d did not do what its commit message claimed. It said
"refuses to start"; it did not. Throwing from `register()` does not stop the
server — the port is already bound by then, so Next logs "Failed to prepare
server" and the process STAYS ALIVE, answering 500 to every route.
That is worse than the problem it replaced. A TCP or HTTP healthcheck sees an
open port and reports the container healthy, so a dead deploy goes green and
keeps receiving traffic, and the operator sees 500s on /login AND
/favicon.ico AND everything else with no indication of why.
Measured, both before and after:
before port LISTENING, GET /login -> 500, GET /favicon.ico -> 500
after nothing listening, connection refused, EXIT_CODE=1
A non-zero exit is what a deployment platform reads as a failed deploy. The
numbered problem list goes to stderr before exiting, so every restart attempt
reprints the reason.
The Node-only half moves to src/instrumentation-node.ts, reached by dynamic
import. instrumentation.ts is compiled for EVERY runtime the app uses, and
src/proxy.ts makes this app compile an Edge one, where process.exit does not
exist — Turbopack flagged it ("A Node.js API is used (process.exit) which is
not supported in the Edge Runtime") even though the call sits behind a
NEXT_RUNTIME guard that can never let it run there. A runtime guard is not a
bundling boundary; a separate module behind a dynamic import is. The build is
warning-free again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three misconfigurations in a row were each diagnosed the same slow way: deploy,
try to sign in, read a status code, guess, read a response body, guess again.
That is a property of the design, not of bad luck. Every required variable is
read lazily, on the first request that needs it, so a container missing its
environment starts clean, serves the sign-in page, and passes a healthcheck
while being completely unable to authenticate anybody.
The lazy reads have to stay — reading at module scope fails `next build`, which
collects page data with NODE_ENV=production and none of these variables set
(that is the "Failed to collect page data for /api/assistant" failure already
documented in apiClient). So the check goes in an instrumentation hook instead,
which Next runs once per server start and NEVER during a build: it returns early
when NEXT_PHASE is 'phase-production-build', in
server/lib/router-utils/instrumentation-globals.external.js.
Boot now either prints what it resolved:
[loyaly] config ok — platform https://mcp.loyaly.ai, auth secret set, NODE_ENV=production
or refuses to start, naming every problem at once rather than one per deploy:
refusing to start — 2 configuration problem(s):
1. LOYALY_API_BASE is invalid: https://api.example.com is not a supported production API host...
2. AUTH_SECRET is required in production — it signs the session cookie...
Verified against a real standalone build, booted four ways: unconfigured, fully
configured, LOYALY_API_BASE pointed at the console, and both wrong at once.
The platform origin and its validation move to shared/config/platformApi. This
is load-bearing, not tidying: apiClient is `server-only`, and that package
resolves to a module which THROWS ON IMPORT outside a react-server condition —
which the instrumentation bundle is not. Importing apiClient from the boot check
would have crashed every start, correctly configured or not. The extracted
module imports nothing but ConfigError, so the boot check and the request path
run the same function against the same allowlist.
Also corrects a claim I made in the Dockerfile two commits ago. `next build`
DOES copy .env into .next/standalone — writeStandaloneDirectory takes exactly
.env and .env.production and nothing else — so the explicit COPY is redundant
rather than required. It stays, with an accurate reason: it only happens when
.env is in the build context, and .dockerignore excluded it until recently.
Keeping the COPY makes that dependency fail the Docker build loudly instead of
producing an unconfigured image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>