---
name: Production-only import-time env throws crash autoscale deploys
description: Why an api-server that runs fine in dev can fail to publish, and how to catch it before deploying.
---

A module that throws at **import time** when an env var is missing in production
(e.g. `if (!process.env.X && NODE_ENV==="production") throw`) will crash the
server before `app.listen`, so the port never opens and the autoscale **promote /
startup health check** fails. The build phase still SUCCEEDS — the failure is at
promote, not build.

**Why dev hides it:** the same guard usually has a `NODE_ENV !== "production"`
fallback (a hardcoded dev value), so dev never trips it. The crash only appears
when `NODE_ENV=production`.

**How to diagnose a "failed to publish" deploy:**
1. `listDeploymentBuilds` / `getDeploymentBuild` — if build logs compile cleanly
   and end at "Creating Autoscale service", it's a promote/health failure, not a
   build error.
2. `fetch_deployment_logs` — look for `required port was never opened` /
   `detected=0` and repeated `healthcheck /api returned status 500`. That means
   the process started but never bound its port (crashed at startup).
3. Reproduce locally — this is the fast path:
   `NODE_ENV=production PORT=<free> node artifacts/<api>/dist/index.mjs` after a
   prod build. The crash + stack trace prints immediately. Then curl the health
   path and assert 200.

**The model-directory deploy specifics:** real publish only builds artifacts that
have a `[services.production]` (api-server via esbuild, model-directory via vite).
The dev-only `mockup-sandbox` (kind=design, no production service) is NOT built at
publish time, so its `vite.config.ts` PORT/BASE_PATH throw is a red herring for
deploy failures (it only bites a root-level `pnpm -r run build`). `.replit` has no
root build hook here — only `postBuild = pnpm store prune`.

**Secret fallback decision:** JWT signing secret resolves
`process.env.JWT_SECRET ?? process.env.SESSION_SECRET`. SESSION_SECRET is
Replit-provisioned in BOTH dev and prod, so this avoids requiring a manual secret
to publish. Acceptable as a fallback; a dedicated JWT_SECRET is cleaner long-term
(secret reuse couples rotation domains). Secrets can't be set via setEnvVars —
they need requestEnvVar (user-provided), which is why the provisioned
SESSION_SECRET fallback is the low-friction fix.
