@lenne.tech/nest-server 11.41.4 → 11.41.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,14 +17,15 @@ The framework pulls in three transitive packages that its `@nestjs/*` dependenci
17
17
  | Package | Advisory | Why it cannot resolve forward on its own |
18
18
  |---------|----------|------------------------------------------|
19
19
  | `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | Older `@nestjs/graphql` releases declare `"ws": "8.20.1"` — an **exact pin**, not a caret. 13.4.5, the version this framework declares, pins `8.21.3` |
20
- | `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS). Patched `>=5.2.2` | Older `@nestjs/swagger` releases declare `"js-yaml": "5.2.1"` — an **exact pin**, the same shape as the `ws` case. 11.4.7, the version this framework declares, pins `5.3.0` |
20
+ | `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS), patched `>=5.2.2`; [GHSA-r3ph-w7gj-g6xm](https://github.com/advisories/GHSA-r3ph-w7gj-g6xm) — moderate: `maxTotalMergeKeys` does not bound CPU use for empty merge sources, covers `<=5.4.0`, patched `>=5.4.1` (published 2026-09-29) | `@nestjs/swagger` declares js-yaml as an **exact pin**, the same shape as the `ws` case: older releases `5.2.1`, 11.4.7 — the version this framework declares, and the newest 11.x — `5.3.0`. No swagger update reaches the fix, so **this entry is load-bearing again** |
21
21
  | `multer` | [GHSA-wc9g-mqfw-jrwm](https://github.com/advisories/GHSA-wc9g-mqfw-jrwm), [GHSA-535w-7cp7-47q4](https://github.com/advisories/GHSA-535w-7cp7-47q4), [GHSA-qfvm-cv95-jqjf](https://github.com/advisories/GHSA-qfvm-cv95-jqjf) — high: DoS via crafted multipart input; [GHSA-qvfw-j98x-7q72](https://github.com/advisories/GHSA-qvfw-j98x-7q72) — low: file size limit bypass. Patched `>=2.3.0` | `@nestjs/platform-express` up to 11.2.5 declares `"multer": "2.2.0"` — an **exact pin**. A direct `multer` dependency does not move it: you get both copies, and FileInterceptor uses the vulnerable one. 11.2.6, the version this framework declares since 11.41.4, pins `2.4.0` |
22
22
 
23
- **Status since 11.41.4:** with the `@nestjs/*` versions this framework declares, all three resolve to
24
- a patched version on their own. The entries below are insurance for a project whose own
25
- `package.json` pins an older `@nestjs/graphql`, `@nestjs/swagger` or `@nestjs/platform-express` —
26
- duplicates of those are exactly how an old exact pin comes back. Keep them; they cost nothing while
27
- inert, and keep the `multer` target in lockstep (now `2.4.0`).
23
+ **Status since 11.41.5:** `ws` and `multer` resolve to a patched version on their own with the
24
+ `@nestjs/*` versions this framework declares; their entries below are insurance for a project whose
25
+ own `package.json` pins an older `@nestjs/graphql` or `@nestjs/platform-express`. **`js-yaml` does
26
+ not**: GHSA-r3ph-w7gj-g6xm covers the `5.3.0` that `@nestjs/swagger` 11.4.7 pins, so without the
27
+ entry your `pnpm audit` reports it. Its key and target were raised in 11.41.5 — an entry copied
28
+ before that (`<5.2.2`) no longer reaches swagger's pin.
28
29
 
29
30
  `@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
30
31
  `graphQl: false`. `@nestjs/swagger` and `@nestjs/platform-express` are likewise plain `dependencies`
@@ -43,10 +44,11 @@ overrides:
43
44
  # Remove once @nestjs/graphql stops pinning it.
44
45
  'ws@>=8.0.0 <8.21.0': '8.21.3'
45
46
 
46
- # Older @nestjs/swagger releases exact-pin js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
47
- # Same shape as the ws entry: an exact pin cannot resolve forward.
48
- # Remove once @nestjs/swagger stops pinning it.
49
- 'js-yaml@>=5.0.0 <5.2.2': '5.2.2'
47
+ # @nestjs/swagger exact-pins js-yaml (11.4.7: 5.3.0) — GHSA-r3ph-w7gj-g6xm (moderate, patched >=5.4.1)
48
+ # and, for older swagger releases, GHSA-pm4m-ph32-ghv5 (high, patched >=5.2.2).
49
+ # Same shape as the ws entry: an exact pin cannot resolve forward. LOAD-BEARING since 11.41.5.
50
+ # Remove once @nestjs/swagger pins >=5.4.1 itself.
51
+ 'js-yaml@>=5.0.0 <5.4.2': '5.4.2'
50
52
 
51
53
  # @nestjs/platform-express <=11.2.5 exact-pins multer@2.2.0 (GHSA-wc9g-mqfw-jrwm and three more, patched >=2.3.0).
52
54
  # Keep the target in LOCKSTEP with the multer version @lenne.tech/nest-server declares (2.4.0 since 11.41.4).
@@ -0,0 +1,144 @@
1
+ # Migration Guide: 11.41.4 → 11.41.5
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
7
+ | **Breaking Changes** | None in the package |
8
+ | **Bugfix** | `migrate down` now holds the migration lock, like `migrate up` already did |
9
+ | **Template change** | nest-server-starter's `docker-entrypoint.sh` now aborts the start when a migration RAN AND FAILED (§2) |
10
+ | **New option** | `MIGRATIONS_ALLOW_FAILURE=true` — start anyway for one deploy (starter and this repo's entrypoint) |
11
+ | **Dependencies** | nodemailer 9 → 10 (security; the only break is Node >= 20), js-yaml override now needed in your project (§3) |
12
+ | **Migration Effort** | Raise two overrides in your `pnpm-workspace.yaml` (§3); decide once whether to adopt the strict entrypoint (§2) |
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server
18
+ pnpm run update # starter-based projects: aligns the framework's ecosystem (see §3)
19
+ pnpm run build && pnpm test
20
+ ```
21
+
22
+ Vendor-mode projects pick up `migration-runner.ts` through the core update.
23
+
24
+ ## What's New in 11.41.5
25
+
26
+ ### 1. `migrate down` holds the migration lock
27
+
28
+ Since 11.33.0, `migrate up` (`MigrationRunner.up()`) runs under the lock of the state store's lock
29
+ collection — `migrations_lock` by default for stores built by `createMigrationStore()` — so replicas
30
+ booting together serialize instead of each applying the same pending migration. `migrate down`
31
+ did not. A rollback is typically run while a deploy is failing, i.e. exactly while replicas restart
32
+ and each boots into `migrate up`; outside the lock the two read and rewrote the migration state
33
+ concurrently. `MigrationRunner.down()` now takes the same lock. `migrate list` only reads and stays
34
+ unlocked.
35
+
36
+ Nothing to do: projects using `createMigrationStore()` get it automatically. A store built with an
37
+ empty lock collection name (`createMigrationStore(uri, 'migrations', '')`) still runs unlocked, as
38
+ before.
39
+
40
+ ### 2. The starter's entrypoint aborts on a migration that ran and failed
41
+
42
+ This concerns `docker-entrypoint.sh` in **nest-server-starter** (and therefore in projects created
43
+ from it), not the npm package. Until now the starter's entrypoint logged a failed migration and
44
+ started the server anyway. A failed schema migration is then indistinguishable from a good deploy
45
+ on every level anyone watches — health check 200, the right commit on `/meta`, drift detection
46
+ green, `turbo deploy --wait` converging — over an app that works against the empty collections
47
+ Mongoose created at boot while the data still sits under the old names.
48
+
49
+ | Situation | Before | Now |
50
+ | --------- | ------ | --- |
51
+ | A migration ran and threw | warning, server starts | **start aborted** (exit 1); the container runtime retries |
52
+ | `MIGRATIONS_ALLOW_FAILURE=true` | — | warning, server starts — a deliberate exception for ONE deploy |
53
+ | `MIGRATE_FAILURE_POLICY=warn` / `abort` | `warn` was the default | still honoured; wins over `MIGRATIONS_ALLOW_FAILURE` |
54
+ | Unknown value in either variable | fell back to `warn` | falls back to `abort`, with a warning |
55
+ | A recorded migration whose FILE is gone | warning, server starts | unchanged: warning, server starts |
56
+ | No migrations bundled / no CLI in the image | skipped | unchanged: skipped |
57
+
58
+ The last-but-one row is deliberate: migrations that ran everywhere may simply be deleted once a new
59
+ instance no longer needs them (git history restores them). The migrate CLI reports such a file as a
60
+ warning, and the entrypoint passes no `--strict`. A project that wants the opposite sets
61
+ `NSC__MIGRATE__STRICT=true` explicitly — that stays available, it is just not the default.
62
+
63
+ **What to do in an existing project:** your `docker-entrypoint.sh` is your own file, so nothing
64
+ changes until you adopt the new one. To adopt it, copy `docker-entrypoint.sh` from
65
+ nest-server-starter 11.41.5. If a deploy must go out with a migration known to fail, set
66
+ `MIGRATIONS_ALLOW_FAILURE=true` for that deploy and unset it again once the migration is fixed.
67
+
68
+ This repository's own `docker-entrypoint.sh` already defaulted to `abort`; it gained the
69
+ `MIGRATIONS_ALLOW_FAILURE` alias.
70
+
71
+ ### 3. Dependency updates
72
+
73
+ | Package | 11.41.4 | 11.41.5 |
74
+ | ------- | ------- | ------- |
75
+ | `nodemailer` | 9.1.1 | **10.0.13** |
76
+ | `mongoose` | 9.10.2 | 9.10.3 |
77
+ | `@modelcontextprotocol/sdk` | 1.30.1 | 1.31.0 |
78
+ | dev: `@swc/core`, `@types/multer`, `@types/node`, `oxfmt`, `oxlint` | 1.16.2, 2.2.0, 26.6.2, 0.70.0, 1.85.0 | 1.16.12, 2.3.0, 26.6.3, 0.71.0, 1.86.0 |
79
+
80
+ **nodemailer 10 is a major, and it ships in a patch on purpose.** Five advisories cover 9.1.1 — two
81
+ of them high (GHSA-v53p-9fqp-m79j, GHSA-prgh-xp8r-p3m5), three moderate — and they are fixed only in
82
+ 10.x; there is no 9.x backport. The single breaking change of 10.0.0 is "Node.js 20 or newer is
83
+ required", and this package already requires Node >= 22.12. 10.x is a TypeScript rewrite that ships
84
+ its own declarations; `@types/nodemailer` is no longer read. Only one thing changes for code that
85
+ uses nodemailer's types directly:
86
+
87
+ ```typescript
88
+ import type * as SMTPTransport from 'nodemailer/lib/smtp-transport';
89
+
90
+ let transport: SMTPTransport; // before: the namespace WAS the class
91
+ let transport: SMTPTransport.default; // nodemailer 10: the class is the default export
92
+ let options: SMTPTransport.Options; // unchanged
93
+ ```
94
+
95
+ `MailTransportOptions` in `IServerOptions` was adjusted the same way; a project that only passes a
96
+ plain options object (`email.smtp`) is unaffected.
97
+
98
+ **What to do:**
99
+
100
+ 1. Starter-based projects: `pnpm run update` raises `mongoose` and the dev tooling to the versions above.
101
+ 2. Two overrides `pnpm run update` does NOT touch — edit `overrides:` in your `pnpm-workspace.yaml`:
102
+
103
+ ```yaml
104
+ # The lockstep entry from nest-server-starter. Left at 9.1.1 it forces the framework's
105
+ # nodemailer back DOWN to 9.1.1 — and keeps all five advisories.
106
+ nodemailer: 10.0.13
107
+
108
+ # @nestjs/swagger 11.4.7 (the newest 11.x) exact-pins js-yaml 5.3.0, covered by
109
+ # GHSA-r3ph-w7gj-g6xm (moderate, patched >=5.4.1). Replaces an older `<5.2.2` entry, which no
110
+ # longer reaches that pin. Background: docs/security-overrides.md.
111
+ 'js-yaml@>=5.0.0 <5.4.2': '5.4.2'
112
+ ```
113
+
114
+ 3. Run `pnpm install` and `pnpm audit` — both advisories must be gone.
115
+
116
+ Overrides in this repository never reach your project (pnpm applies them only to the root of an
117
+ install), which is why the js-yaml entry has to be repeated there.
118
+
119
+ ## Compatibility Notes
120
+
121
+ | Pattern | Status |
122
+ | ------- | ------ |
123
+ | `createMigrationStore(uri)` / `createMigrationStore(uri, 'migrations')` | Lock active for `up` and now `down` |
124
+ | A custom `MongoStateStore` without `lockCollectionName` | Unchanged: runs unlocked |
125
+ | `NSC__MIGRATE__STRICT=true` | Unchanged: a missing recorded migration file fails the run |
126
+ | A `nodemailer: 9.1.1` override (starter-derived projects) | Pins nodemailer 9.1.1 under the framework — raise it to `10.0.13` (§3) |
127
+ | Code typing `import * as X from 'nodemailer/lib/…'` as `X` | Use `X.default` (§3) |
128
+ | Your own `docker-entrypoint.sh` | Unchanged until you adopt the starter's (§2) |
129
+
130
+ ## Troubleshooting
131
+
132
+ | Symptom | Cause | Fix |
133
+ | ------- | ----- | --- |
134
+ | Container restarts in a loop, log: `refusing to start against a possibly half-applied schema` | A migration ran and failed (new entrypoint) | Fix the migration; for one deploy, `MIGRATIONS_ALLOW_FAILURE=true` |
135
+ | `migrate down` waits with `Waiting for migration lock release …` | Another replica is running `migrate up` | Wait; a lock whose holder died is broken after 60 s without heartbeat |
136
+ | `Strict mode: N recorded migration file(s) missing` | `NSC__MIGRATE__STRICT=true` and a migration file was deleted | Restore the file from git, or unset `NSC__MIGRATE__STRICT` |
137
+ | `pnpm audit` still reports nodemailer advisories after the update | A `nodemailer: 9.1.1` override pins the old version | Raise it to `10.0.13` |
138
+ | `pnpm audit` reports GHSA-r3ph-w7gj-g6xm (js-yaml) | No (or an old `<5.2.2`) js-yaml override | Add `'js-yaml@>=5.0.0 <5.4.2': '5.4.2'` |
139
+ | `TS2709` / `Cannot use namespace 'SMTPTransport' as a type` | nodemailer 10 types | Use `SMTPTransport.default` |
140
+
141
+ ## Module Documentation
142
+
143
+ - [Migrate module README](../src/core/modules/migrate/README.md) — locking, strict mode, CLI
144
+ - nest-server-starter `README.md` → Environment — `MIGRATE_FAILURE_POLICY` / `MIGRATIONS_ALLOW_FAILURE`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.41.4",
3
+ "version": "11.41.5",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -25,12 +25,13 @@
25
25
  "cf": "pnpm run check:fix",
26
26
  "check": "node scripts/check.mjs",
27
27
  "check:consumer": "node scripts/check-consumer.mjs",
28
- "check:fix": "pnpm install && pnpm run spectaql:sync && pnpm audit --fix && pnpm run check:overrides && pnpm peers check && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
28
+ "check:fix": "pnpm install && pnpm run spectaql:sync && pnpm audit --fix && pnpm run check:overrides && pnpm peers check && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && pnpm run check:server-start",
29
29
  "check:manifest": "node scripts/check-package-manifest.mjs",
30
30
  "check:mutations": "node scripts/check-mutations.mjs",
31
- "check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run check:overrides && pnpm peers check && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
31
+ "check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run check:overrides && pnpm peers check && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && pnpm run check:server-start",
32
32
  "check:overrides": "node scripts/check-overrides.mjs",
33
- "check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run check:overrides && pnpm peers check && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
33
+ "check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run check:overrides && pnpm peers check && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && pnpm run check:server-start",
34
+ "check:server-start": "cross-env NODE_ENV=local node scripts/check-server-start.mjs --entry=dist/main.js --port-env=NSC__PORT --ready=\"Server starte[dt] at\" --path=/",
34
35
  "check:swc-tdz": "nest build -b swc -p tsconfig.swc-tdz.json && node scripts/check-swc-tdz.mjs",
35
36
  "cnaf": "pnpm run check:naf",
36
37
  "docs": "pnpm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
@@ -92,7 +93,7 @@
92
93
  "@apollo/server": "5.5.1",
93
94
  "@as-integrations/express5": "1.1.2",
94
95
  "@getbrevo/brevo": "6.0.3",
95
- "@modelcontextprotocol/sdk": "1.30.1",
96
+ "@modelcontextprotocol/sdk": "1.31.0",
96
97
  "@nestjs/apollo": "13.4.5",
97
98
  "@nestjs/common": "11.2.6",
98
99
  "@nestjs/core": "11.2.6",
@@ -126,10 +127,10 @@
126
127
  "json-to-graphql-query": "2.3.0",
127
128
  "lodash": "4.18.1",
128
129
  "mongodb": "7.6.0",
129
- "mongoose": "9.10.2",
130
+ "mongoose": "9.10.3",
130
131
  "multer": "2.4.0",
131
132
  "node-mailjet": "6.0.11",
132
- "nodemailer": "9.1.1",
133
+ "nodemailer": "10.0.13",
133
134
  "passport": "0.7.0",
134
135
  "passport-jwt": "4.0.1",
135
136
  "reflect-metadata": "0.2.2",
@@ -177,8 +178,8 @@
177
178
  }
178
179
  },
179
180
  "devDependencies": {
180
- "@aws-sdk/client-s3": "3.1140.0",
181
- "@aws-sdk/s3-request-presigner": "3.1140.0",
181
+ "@aws-sdk/client-s3": "3.1143.0",
182
+ "@aws-sdk/s3-request-presigner": "3.1143.0",
182
183
  "@better-auth/core": "1.7.1",
183
184
  "@better-auth/passkey": "1.7.1",
184
185
  "@compodoc/compodoc": "2.0.0",
@@ -186,21 +187,21 @@
186
187
  "@nestjs/schematics": "11.1.0",
187
188
  "@nestjs/testing": "11.2.6",
188
189
  "@swc/cli": "0.8.1",
189
- "@swc/core": "1.16.2",
190
+ "@swc/core": "1.16.12",
190
191
  "@tus/s3-store": "2.0.7",
191
192
  "@types/compression": "1.8.1",
192
193
  "@types/cookie-parser": "1.4.10",
193
194
  "@types/ejs": "3.1.5",
194
195
  "@types/express": "5.0.6",
195
196
  "@types/lodash": "4.17.25",
196
- "@types/multer": "2.2.0",
197
- "@types/node": "26.6.2",
197
+ "@types/multer": "2.3.0",
198
+ "@types/node": "26.6.3",
198
199
  "@types/nodemailer": "8.0.2",
199
200
  "@types/passport": "1.0.17",
200
201
  "@vitest/coverage-v8": "4.1.11",
201
202
  "ansi-colors": "4.1.3",
202
203
  "better-auth": "1.7.1",
203
- "bullmq": "6.3.8",
204
+ "bullmq": "6.3.10",
204
205
  "cross-env": "10.1.0",
205
206
  "find-file-up": "2.0.1",
206
207
  "husky": "9.1.7",
@@ -208,8 +209,8 @@
208
209
  "nodemon": "3.1.14",
209
210
  "npm-watch": "0.13.0",
210
211
  "otpauth": "9.5.2",
211
- "oxfmt": "0.70.0",
212
- "oxlint": "1.85.0",
212
+ "oxfmt": "0.71.0",
213
+ "oxlint": "1.86.0",
213
214
  "rimraf": "6.1.3",
214
215
  "ts-node": "10.9.2",
215
216
  "tsconfig-paths": "4.2.0",
@@ -37,19 +37,25 @@ export type BetterAuthFieldType = 'boolean' | 'date' | 'json' | 'number' | 'numb
37
37
  * particularly useful in CI / e2e tests: `{ jsonTransport: true }` serializes
38
38
  * outgoing mail to a JSON string and returns a valid response without any
39
39
  * network I/O — no SMTP server, no credentials, no flakiness.
40
+ *
41
+ * The `X.default` members are the transport INSTANCE types. nodemailer >= 10 ships its own
42
+ * declarations, where each `nodemailer/lib/<transport>` module is `export default class` plus a
43
+ * module-level `Options` alias, so `import type * as X` yields a module namespace — the class is
44
+ * `X.default`. (`@types/nodemailer` used `export =` with a merged namespace, where `X` itself
45
+ * was the class.)
40
46
  */
41
47
  export type MailTransportOptions =
42
- | JSONTransport
48
+ | JSONTransport.default
43
49
  | JSONTransport.Options
44
- | SendmailTransport
50
+ | SendmailTransport.default
45
51
  | SendmailTransport.Options
46
- | SESTransport
52
+ | SESTransport.default
47
53
  | SESTransport.Options
48
- | SMTPPool
54
+ | SMTPPool.default
49
55
  | SMTPPool.Options
50
- | SMTPTransport
56
+ | SMTPTransport.default
51
57
  | SMTPTransport.Options
52
- | StreamTransport
58
+ | StreamTransport.default
53
59
  | StreamTransport.Options
54
60
  | string;
55
61
 
@@ -240,9 +240,12 @@ How the lock works, regardless of which entry point uses it:
240
240
 
241
241
  This ensures that in a cluster with multiple nodes, migrations run on only one machine at a time.
242
242
 
243
- **Active by default for `migrate up`.** Stores built by `createMigrationStore()` use the
244
- lock collection `migrations_lock` unless another name is given, and `MigrationRunner.up()`
245
- (the CLI's `up` command) acquires that lock around the whole run. This matters because the
243
+ **Active by default for `migrate up` and `migrate down`.** Stores built by `createMigrationStore()` use the
244
+ lock collection `migrations_lock` unless another name is given, and `MigrationRunner.up()` / `.down()`
245
+ (the CLI's `up` / `down` commands) acquire that lock around the whole run. `down` joined later (DEV-2728): a
246
+ rollback is typically run while a deploy is failing, i.e. while replicas restart and each boots into
247
+ `migrate up` — outside the lock the two rewrote the migration state concurrently. `migrate list` only
248
+ reads and stays unlocked. This matters because the
246
249
  container entrypoint runs migrations on **every** boot: without the lock, N replicas
247
250
  starting together each read the same empty state and apply the same pending migration N
248
251
  times. A replica that waited re-reads the state inside the lock and finds nothing pending.
@@ -530,10 +533,15 @@ Enable it via any of:
530
533
  | `NSC__MIGRATE__STRICT=1\|true\|yes` env var | CLI **and** programmatic runners (resolved in the `MigrationRunner` constructor) |
531
534
  | `new MigrationRunner({ strict: true, ... })` | programmatic |
532
535
 
533
- **Recommended for production images:** set `NSC__MIGRATE__STRICT=true` in the container
534
- environment. In an immutable image, a recorded-but-missing migration file can only mean a
535
- broken build (empty/miscopied `migrations/` directory) or a state-store mismatch (wrong
536
- database) — both are conditions where refusing to boot is correct.
536
+ **Off by default, on only when a project explicitly wants it.** Deleting migration files that
537
+ ran everywhere is a normal practice — a new instance never needs them, and git history restores
538
+ them if ever needed — and under strict mode every such deletion would refuse the boot. Turn it on
539
+ for images that must carry the FULL migration history. Where the history is complete by
540
+ design, a recorded-but-missing file can only mean a broken build (empty/miscopied
541
+ `migrations/` directory) or a state-store mismatch (wrong database), and refusing to boot is
542
+ correct. This is independent of the entrypoint's failure policy: a migration that RAN AND
543
+ FAILED aborts the container start by default (`MIGRATIONS_ALLOW_FAILURE=true` opts out per
544
+ deploy), whatever `strict` says.
537
545
 
538
546
  ### Programmatic usage (MigrationRunner)
539
547
 
@@ -297,8 +297,17 @@ export class MigrationRunner {
297
297
 
298
298
  /**
299
299
  * Rollback the last migration (down)
300
+ *
301
+ * Serialized through the same lock as `up()`. A rollback is typically run while a deploy
302
+ * is failing, i.e. exactly while replicas restart and each boots into `migrate up`; outside
303
+ * the lock the two would read and rewrite the migration state concurrently, and one of them
304
+ * would save a state that no longer matches the database.
300
305
  */
301
306
  async down(): Promise<void> {
307
+ await withMigrationLock(this.options.stateStore, () => this.runDown());
308
+ }
309
+
310
+ protected async runDown(): Promise<void> {
302
311
  const { _endMigration, _startMigration } = await import('./helpers/migration.helper');
303
312
 
304
313
  const state = await this.options.stateStore.loadAsync();