fix(config): select environment.prod.ts in the compiled @gauzy/config for production builds

Merged
EV
10d ago
+464-23

Problem

Every production image of the API and the worker runs with the development environment: environment.production === false, envName: 'dev'.

Two things combined:

  1. apps/api/project.json and apps/worker/project.json ask webpack to replace packages/config/src/environments/environment.ts with environment.prod.ts — a directory that does not exist (the files are under packages/config/src/lib/environments/), so the replacement never matched anything.
  2. Even with the right path it could not work: the API and worker bundles externalize @gauzy/*. @gauzy/config is compiled on its own by nx build config (@nx/js:tsc, which has no file replacement) and dist/packages/config is copied into dist/apps/<app>/node_modules/@gauzy/config by the apps' webpack configs. A fileReplacements entry in the consumer's build never reaches that copy. The package's own lib:build:prod script (scripts/replace-env-files.ts) also pointed at the missing src/environments directory, so it would have thrown had anything called it.

Evidence: every local --configuration=production build in this workspace has production: false, envName: 'dev' in dist/packages/config/src/lib/environments/environment.js and in the API's node_modules/@gauzy/config copy; the production API created gauzydevsess: session keys in Redis during the #10976 benchmarks.

Fix

The selection happens where the file actually lives — on the compiled package — and per application, at copy time, so the shared dist/packages/config is never rewritten by an app build (review finding by Greptile: two concurrent builds with different configurations must not affect each other).

  • packages/config/src/lib/environments/select-compiled-environment.ts — compiled with the package, used from dist/ with plain node. compiledEnvironmentContent(dir, environment) returns what environment.js must contain for the requested environment (production: environment.prod.js behind a marker line; development: the compiled environment.ts, or the preserved copy of it if the dist was switched to production), reading only. selectCompiledEnvironment writes it for a standalone production dist.
  • apps/api/config/webpack-package-copy-patterns.js (shared by the worker's webpack config): getCopyPatterns(..., { environment }) adds a CopyWebpackPlugin transform for the @gauzy/config package only, which writes environment.js in the app's own node_modules as compiledEnvironmentContent(..., environment). Both webpack configs pass ctx.context.configurationName === 'production' ? 'production' : 'development' and log @gauzy/config environment for this build: ….
  • config:select-environment target (nx:run-commands, not cached, dependsOn: build, development/production configurations) rewrites dist/packages/config itself — for a standalone production dist of the package; lib:build:prod now runs it. App builds do not depend on it.
  • The fileReplacements paths are corrected too (they apply if a workspace package is ever bundled from source instead of externalized), README documents the mechanism.

config:build itself is untouched (renaming the tsc target broke dependency resolution: @nx/js:tsc finds a dependency's compiled output by a target of the same name).

Verification

  • nx build worker -c production --skip-nx-cache (full webpack build): logs @gauzy/config environment for this build: production; dist/apps/worker/node_modules/@gauzy/config/src/lib/environments/environment.js starts with the marker and has production: true, while dist/packages/config/.../environment.js is unchanged (development).
  • Direct call of the copy transform against the real dist: production → marker + production: true; development → the compiled development file; other files of the package pass through untouched; without environment no pattern has a transform.
  • nx run config:select-environment -c production --skip-nx-cache → dist environment.js starts with the marker and loads under node (four secrets set) as production=true envName=prod THROTTLE_ENABLED=true isDevelopment()=false; without secrets it throws Production startup aborted: insecure JWT secrets detected as environment.prod.ts intends. nx run config:select-environment (development) → production: false; the same with a cached config:build after a production selection → restored.
  • select-compiled-environment.spec.ts: 14 tests (content for either environment in either dist state, substitution, restore, idempotency, tsc re-emit between selections, missing-file errors, argument parsing) — pass.
  • git status after the builds: only the intended files changed; no environment.ts edits.

What changes at runtime in production images (stage first)

Everything environment.prod.ts differs in from environment.ts becomes effective for the API and the worker:

  • validateProductionSecrets() at module load: JWT_SECRET, JWT_REFRESH_TOKEN_SECRET, JWT_VERIFICATION_TOKEN_SECRET, EXPRESS_SESSION_SECRET must be set and not well-known defaults, or the process refuses to start. The stage and prod manifests set all four from secrets; bootstrap/validate-secrets.ts already enforced the same under NODE_ENV=production.
  • THROTTLE_ENABLED defaults to on (THROTTLE_TTL 60 s, THROTTLE_LIMIT 60000 unless set).
  • Express session keys use the gauzyprodsess: prefix (one-time re-login for sessions stored under the old prefix).
  • i18n watch: false (app.module.ts, seeder.module.ts); isDevelopment() is false; seeding and job-search take their production paths (no random fake job posts when the Gauzy AI integration is down); baseUrl/clientBaseUrl defaults are the gauzy.co URLs (env vars override as before).
  • Demo is built with NODE_ENV=development and is unaffected. Desktop/Electron server builds have their own webpack configs and keep the development environment as today.

Per the owner's rule for Gauzy API/UI: develop → stage only; watch stage for the points above before any production promotion.

scripts/replace-env-files.ts is left in place (unused, and it edits source files); it can go in a later cleanup if wanted.

🤖 Generated with Claude Code