04-May-2026 — make Create Environment wizard work end-to-end closes #258
build 637 — commit 8fb1097
The Environment Setup wizard at /admin/environment-management/setup
did not work on any deployed machine. Five failure modes stacked on
top of each other depending on how far a run got. This change addresses
all of them so that clicking Create Environment, on a deployed machine,
ends up with a fully working Fly app, and clicking Resume on a
half-completed environment finishes cleanly with no manual flyctl
intervention.
Failure mode 1: persistent non-vcs/fly-io/configs.json read
The wizard ran inside the production Fly machine. non-vcs/ is
gitignored and never makes it into the Docker image, so the deploy
died on ENOENT and the residual code path then called
process.exit(1), taking the whole production server with it. Both
wizard entry points (handleEnvironmentCreate and
handleEnvironmentSetup) hit the same read at deployToFlyio:141.
What changed
- Deleted
server/lib/shared/configs-json.tsoutright. - Deleted
server/deploy/manage-configs.ts(the only remaining writer ofconfigs.jsonoutside the wizard) and its npm script. server/lib/cli/commands/fly.tsnow readsdockerImagefrom theConfigKey.ENVIRONMENTSdocument via a newdockerImageFromDatabase()helper. After a successful deploy it persists the env entry throughupsertEnvironmentInDatabaseinstead of writing a JSON file.- Added optional
dockerImageandregionfields toEnvironmentsConfigplus aDEPLOYMENT_DEFAULTSconstant (nbarrett36/ngx-ramblers:latest/lhr) matching what the published image actually is and the region the user has been deploying to.regionis wired throughbuildDeploymentConfigso the legacy CLI deploy path also reads it from the DB. server/lib/cli/commands/destroy.tslost its--skip-configsflag and now removes the env entry throughremoveEnvironmentFromDatabase.server/lib/environments/environments-config.tsrewritten:loadFromFiles()and the interactive "fall back to file-based configs.json?" prompt removed;configuredEnvironments()throws on a missing DB document instead of exiting; addedupsertEnvironmentInDatabaseandremoveEnvironmentFromDatabase.server/lib/backup/config-initializer.tsseeds from the DB.server/deploy/config-loader.tsandserver/deploy/mongo-cli.tsread configs through the DB-backedloadConfigs()helper.server/deploy/deploy-to-environments.tsandserver/deploy/deploy-integration-worker.tshad duplicate hard-coded image strings. Both now read fromDEPLOYMENT_DEFAULTS.DOCKER_IMAGE.
Failure mode 2: errors never reached the browser
readConfigFile and several other helpers called process.exit(1)
on failure. From inside an HTTP/WebSocket request handler that killed
the production process before the wizard's sendError(ws, ...) could
fire. Fly then restarted the machine.
What changed
readConfigFileremoved entirely.runCommandlost itsthrowOnErrorboolean and now always throws. Fly volume helpers throw rather than exit.configuredEnvironments()andfindEnvironmentFromDatabase()throw on missing DB config instead of exiting.- Audited every
process.exit(...)call inserver/libandserver/deploy. The remaining exits are all in CLI-only entry points; none reachable from request handlers.
Failure mode 3: AUTH_SECRET (and other generated secrets) were not persisted
The wizard generated AUTH_SECRET and a full secrets bundle, wrote
them to a local file under non-vcs/secrets/ (useless on a deployed
machine), and passed them in-memory to deployToFlyio. The first
deploy worked because flyctl secrets import ran with that in-memory
bundle, but the resume flow loaded secrets from the DB - and the DB
record never had AUTH_SECRET or the rest of the bundle written
into it.
What changed
updateEnvironmentsConfiginserver/lib/environment-setup/environment-setup-service.tsnow persists the fullsecretsbundle toenvConfig.secrets. The localwriteSecretsFilecall is gone, and thewriteSecretsFileandsecretsPathimports with it.deployToFlyioperforms aREQUIRED_SECRETSpre-flight check before runningflyctl deploy. If any required secret is missing it throws with a clear list of missing keys, which the wizard relays throughsendError(...).- The Fly secrets-import file is now an
os.tmpdir()temp file unlinked in afinallyblock; no per-app secrets file lingers on disk. - The CLI
ngx-cli fly deploy <name>action usesloadSecretsWithFallbackso it works on a deployed machine where the local file no longer exists.
Failure mode 4: NODE_ENV-dependent path resolver caused silent SPA 404s
Discovered while testing on a freshly resumed environment. After the
AUTH_SECRET pre-flight passed, the deployed Fly app booted but every
request to / returned Cannot GET /. Root cause:
server/lib/shared/path-utils.ts had a resolveClientPath whose
output depended on a runtime check of NODE_ENV === "production".
The wizard's buildSecretsConfig always sets NODE_ENV: "production"
for new environments, but environments created during the original bug
window had an incomplete persisted secrets bundle and the resume flow
imported only the keys it could reconstruct, which did not include
NODE_ENV. The Fly app booted with NODE_ENV empty, the resolver took
the dev branch, the resolved distFolder did not exist, the
static-asset middleware was skipped, and Express fell through to
default 404.
What changed
- Rewrote
server/lib/shared/path-utils.tsto anchor on the project root by walking up from__dirnamelooking forfly.toml- a stable marker present in dev (at the repo root) and in the deployed image (at/usr/src/app). ThenavigateUpFromCurrentExecutionDirectoryhelper and theisProduction()branch are gone.resolveClientPathreturns paths relative to the project root,resolveServerPathreturns paths relative to<projectRoot>/server. Same input produces the same output regardless ofNODE_ENV. - Added
NODE_ENVtoREQUIRED_SECRETSinserver/lib/shared/secrets.ts. The path resolver no longer cares aboutNODE_ENV, but the surrounding app code does (env-config, isProduction guards elsewhere) and a missingNODE_ENVis a real misconfiguration worth catching at deploy time rather than as a silent runtime regression.
Failure mode 5: brevo template migration ENOENT shared the same root cause
The migration
server/lib/mongo/migrations/database/20260120000000-update-brevo-transactional-template.ts
reads brevo HTML templates via localTemplatePath in
server/lib/brevo/templates/local-template-reader.ts, which calls
resolveClientPath. On the deployed container during the wizard's
initial bootstrap, NODE_ENV was unset, so resolveClientPath
returned /usr/src/app/server/ts-gen/projects/ngx-ramblers/src/brevo/templates/fully-automated-text-body.html -
a path that does not exist in the image. ENOENT, migration FAILED,
wizard's "Migration Failed" page rendered. A retry succeeded only
because by then the user was running locally, where the resolver
landed somewhere that happened to have the files.
What changed
The failure mode 4 fix is the entire fix here - localTemplatePath
now lands at <projectRoot>/projects/ngx-ramblers/src/brevo/templates/<name>.html
consistently, regardless of NODE_ENV or which machine the migration
runs on. No changes to the migration or the brevo resolver were
needed.
Cleanup
- Removed the dead duplicate
generateAuthSecretfunction inserver/lib/cli/commands/fly.ts. The active one is inenvironment-setup-service.ts.
How they slipped through
All five failure modes share the same shape: deployment-environment-only
bugs that dev laptops never exercise, because dev laptops happen to
satisfy the fragile assumptions by accident - they have the local
non-vcs/ files, they always run with NODE_ENV set in the shell,
and they have the source tree sitting at the resolved paths regardless
of how the resolver computes them. Wizard / migration changes need at
least one CI run that exercises a clean container - no host
filesystem, no inherited shell env - before being declared green.
Verification
npm run lintcleannpm run lint:servercleannpx tsc --noEmitforserver/tsconfig.jsonand the frontend tsconfig cleannpm run test:serverpasses 619 mocha testsgrep -rn "configs\.json" server/(excluding tooling JSONs) andgrep -rn "non-vcs/fly-io" server/both emptygrep -rn "writeSecretsFile" server/lib/environment-setup/emptygrep -rn "nbarrett.*/ngx-ramblers" server/ projects/returns exactly one match: the singleDEPLOYMENT_DEFAULTS.DOCKER_IMAGEdeclarationgrep -rn "navigateUpFromCurrentExecutionDirectory\|isProduction" server/lib/shared/empty - the path resolver no longer readsNODE_ENV
End-to-end behaviour
Clicking Create Environment in the wizard, on a deployed machine,
ends up with a fully working Fly app: the env record and full secrets
bundle (including NODE_ENV) are written to the DB, dockerImage is
read from the DB, the deploy runs with secrets passed in-memory and
imported to Fly, the env entry is upserted with the deploy token at
the end, the SPA serves /, and migrations that read bundled assets
resolve them from the project root regardless of NODE_ENV state.
Clicking Resume on a half-completed env reads the full secrets bundle
back from the DB and produces a working app without any manual
flyctl secrets set. A simulated failure inside deployToFlyio (bad
Fly API token, missing required secret) produces a red error in the
wizard UI with the failure details, and the production server stays
up.
Out of scope
- The vestigial
EnvironmentSetupResult.configsJsonUpdatedboolean was left as-is. Renaming it touches the wizard component and websocket payload shape, which would broaden the patch.