v0.3.0

Released: May 3, 2026


Node.js 22 floor + sync robustness

Two upgrades aimed at a smoother first-run experience.

Node.js 22 minimum (breaking)

Templates and generated dockerfiles already required Node 22, but the init prerequisite check let Node 20 through, which produced a misleading cascade of errors mid-install. The check now matches reality.

If you're on Node 20: upgrade to 22+ before running npx tsdevstack init.

  • nvm: nvm install 22 && nvm use 22
  • Homebrew: brew install node@22

The four CLI plugins (@tsdevstack/cli, cli-cloud-secrets, cli-infra, cli-mcp) now declare engines: { node: ">=22.0.0" } so npm itself warns before our runtime check fires. Generated Next.js / NestJS dockerfiles and the AWS db-init / db-delete generators all build on node:22 images now.

sync actually waits for databases on cold start

The generated postgres and redis services now declare healthchecks (pg_isready and redis-cli ping respectively). docker compose up --wait previously returned the moment a container process started — not when the service was ready to accept connections — which on first run could let prisma migrate fire before initdb finished, producing a misleading P1010 "denied access" error. With healthchecks declared, --wait blocks until services are actually up.

No action needed — the next npx tsdevstack sync regenerates compose with healthchecks and you're done.

init no longer claims success after silent failures

npx tsdevstack init previously printed "🎉 Project created successfully!" even if npm install, build, sync, or the initial migration had failed mid-run. It now tracks per-step failures and switches to a "created with issues" summary listing what failed and how to recover. The happy path is unchanged.


Private npm packages — auto-detected, no config

If your project depends on private npm packages — internal company scopes, private GitHub Packages, restricted-access third-party SDKs — tsdevstack now wires authentication through the entire pipeline automatically. Same flow locally and in CI, identical across GCP, AWS, and Azure.

Trigger: commit an .npmrc at your project root with ${NPM_TOKEN}-style env-var interpolation:

//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Commit the

.npmrc, not gitignore it The file contains no actual secret — ${NPM_TOKEN} is an env-var reference that npm interpolates at install time. Committing the template is the standard npm-ecosystem pattern. The reason .npmrc is sometimes gitignored is to catch npm login-generated files that contain real tokens; a hand-written template with ${NPM_TOKEN} is safe to commit.

If your .gitignore blanket-excludes **/.npmrc, add an exception:

**/.npmrc
!/.npmrc        # commit the project-root template (env-var interpolation only)

Setup:

  • Locally: export NPM_TOKEN=… in your shell (or in ~/.zshrc / ~/.bashrc). If you skip this, npm install fails with a 404 Not Found on private packages — that's the symptom of an unset NPM_TOKEN, not a genuinely missing package. (Project .npmrc overrides home .npmrc for the same registry, so even a working ~/.npmrc won't help — the project file with empty interpolation wins.)
  • In CI: add NPM_TOKEN as a single GitHub repository secret (one value, not per-environment).

That's it. Run npx tsdevstack infra:generate-docker and infra:generate-ci and the wiring lands automatically.

What it does for you

  • Generated Dockerfiles include .npmrc in the deps stage and emit a BuildKit env-source secret mount on npm ci:
    RUN --mount=type=cache,target=/root/.npm \
        --mount=type=secret,id=npm_token,env=NPM_TOKEN \
        npm ci
  • infra:build-docker appends --secret id=npm_token,env=NPM_TOKEN to the docker build invocation. NPM_TOKEN is read from your shell env via process.env.
  • Generated CI workflows expose NPM_TOKEN at the job level on every job that runs npm ci or invokes tsdevstack:
    jobs:
      deploy:
        runs-on: ubuntu-latest
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        steps: …
Custom (non-generated) CI workflows

Workflows that aren't produced by infra:generate-ci — your own publish pipelines, doc deploys, third-party CI integrations — won't be touched by tsdevstack. If any of them run npm ci (or npm publish) on a project with a committed .npmrc, you need to add NPM_TOKEN: ${{ secrets.NPM_TOKEN }} to those jobs' env: block manually. Without it, npm reads the project .npmrc, finds an empty ${NPM_TOKEN} interpolation, and the registry returns 404 Not Found (npm hides "unauthorized" behind 404, so the error is misleading).

Properties

  • The token lives only during the npm ci RUN step's env — never persisted in image layers, never visible in docker history.
  • The .npmrc file is included in the deps and build intermediate stages but not the production stage, so the final shipped image is clean. The file content itself is just ${NPM_TOKEN} template (no actual secret), so even cache layers pushed to a registry leak nothing sensitive.
  • If .npmrc is present but NPM_TOKEN is unset at build time, BuildKit fails fast with a clear error — misconfiguration is loud, not silent.
  • If .npmrc is absent, generated workflows and Dockerfiles emit no NPM_TOKEN wiring at all (behavior identical to public-only projects, no opt-out flag needed).

Why a single NPM_TOKEN secret in CI (not per-env)

Registry tokens identify your developer / org account, not a deployment target. The same token works for installs to dev, staging, and prod. Unlike provider credentials (which are per-env via GCP_WIF_DEV, AWS_ROLE_ARN_DEV, etc.), NPM_TOKEN is a single global GitHub repo secret.

Migration for existing projects

If you don't have private npm dependencies, no action needed — behavior is unchanged.

If you do:

  1. Create .npmrc at project root with //registry.npmjs.org/:_authToken=${NPM_TOKEN} (or your registry equivalent).
  2. Commit .npmrc to git. If .gitignore blanket-excludes **/.npmrc, add an exception (!/.npmrc) so the project-root template is tracked. The file has no actual secret — only the ${NPM_TOKEN} env-var reference.
  3. export NPM_TOKEN=… locally and add NPM_TOKEN as a GitHub repo secret in CI.
  4. Regenerate: npx tsdevstack infra:generate-docker && npx tsdevstack infra:generate-ci.

See CI/CD Setup — Private npm packages for the full setup walkthrough.


NestJS Dockerfile generator: cleaner production stage

The NestJS Dockerfile generator no longer emits a workspace-level node_modules overlay COPY in the production stage:

 FROM base AS production
 …
 COPY --chown=node:node --from=build /app/node_modules ./node_modules
-COPY --chown=node:node --from=build /app/apps/<service>/node_modules ./node_modules
 COPY --chown=node:node --from=build /app/.tsdevstack/config.json ./.tsdevstack/config.json

The line was a defensive overlay against an older npm-hoisting scenario where Prisma's client could land at the workspace level instead of the root. Under current Prisma versions (the schema's output = "../src/generated/prisma" writes the client into the source tree, not into node_modules) and current npm hoisting behavior, the line never overlaid anything real — the source path doesn't exist in the build stage on cold builds.

When a registry-cached production layer existed (typical for projects already deploying), BuildKit reused the cached layer and the no-op COPY never ran. On cold builds — fresh GCP/AWS/Azure registries, CI runners with no cache, or any path that bypasses --cache-from (e.g. local docker build --no-cache) — BuildKit re-executed the COPY and aborted with failed to compute cache key: … not found. That class of failure is now resolved.

Action

Regenerate Dockerfiles in projects that include hasDatabase: true services:

npx tsdevstack infra:generate-docker

Then redeploy normally (npx tsdevstack infra:deploy --env <env>). Existing deployments don't need to be redeployed unless you're cutting a fresh registry — in which case the regenerated Dockerfiles are required.