Summary
A schema index change that alters the options of an existing same-name index (e.g. adding a partialFilterExpression to an index an already-deployed database still holds without it) currently makes the app unable to boot on that database — even when a migration exists that repairs exactly that conflict.
Root cause
Boot order (lib/app.js#bootstrap, #3990/#3991): awaitIndexBuilds() runs before migrations.run(). When the live collection holds an index under the declared name with different options, MongoDB rejects the build outright:
MongoServerError (code 85, IndexOptionsConflict):
An existing index has the same name as the requested index.
awaitIndexBuilds() propagates the rejection (documented, deliberate in #3990: "a genuine rejection that happens BEFORE the timeout still propagates") → bootstrap() throws → the process never serves.
The trap: index-swap migrations (e.g. modules/billing/migrations/20260727120000-fix-usage-month-index-partial-filter.js) are written on the assumption that the boot build succeeds (creating an empty twin the migration then drops). When a legacy same-name index exists with different options, the build throws instead — the migration that would repair the conflict is gated behind the very defect it repairs. Net effect: any schema change to an existing index's options is un-shippable against an already-deployed database.
Proposed fix
awaitIndexBuilds() must tolerate same-name index conflict errors (codes 85 IndexOptionsConflict / 86 IndexKeySpecsConflict) instead of rejecting boot:
- log at error level, naming the model, the index, and both specs (declared vs live)
- let boot continue — the app serves with the stale live index (writes keep obeying the old constraint), and the migration runner, which runs right after, can reconcile
This is a deliberate contract change vs #3990: those two codes are environment-state conflicts (the fix is a migration), not code bugs. Genuinely invalid declarations (unsupported operators etc.) must keep failing fast — the tolerance is scoped to 85/86 only.
Alternative considered
Running migrations before index builds would also unblock this case, but it inverts the #3990 invariant (unique-index idempotency guards exist before anything writes) and silently invalidates the ordering assumptions already codified in shipped migrations. Rejected in favor of the narrow tolerance above.
Regression test
A schema-declared index whose options differ from a pre-existing same-name index must not fail startup.
Summary
A schema index change that alters the options of an existing same-name index (e.g. adding a
partialFilterExpressionto an index an already-deployed database still holds without it) currently makes the app unable to boot on that database — even when a migration exists that repairs exactly that conflict.Root cause
Boot order (
lib/app.js#bootstrap, #3990/#3991):awaitIndexBuilds()runs beforemigrations.run(). When the live collection holds an index under the declared name with different options, MongoDB rejects the build outright:awaitIndexBuilds()propagates the rejection (documented, deliberate in #3990: "a genuine rejection that happens BEFORE the timeout still propagates") →bootstrap()throws → the process never serves.The trap: index-swap migrations (e.g.
modules/billing/migrations/20260727120000-fix-usage-month-index-partial-filter.js) are written on the assumption that the boot build succeeds (creating an empty twin the migration then drops). When a legacy same-name index exists with different options, the build throws instead — the migration that would repair the conflict is gated behind the very defect it repairs. Net effect: any schema change to an existing index's options is un-shippable against an already-deployed database.Proposed fix
awaitIndexBuilds()must tolerate same-name index conflict errors (codes 85IndexOptionsConflict/ 86IndexKeySpecsConflict) instead of rejecting boot:This is a deliberate contract change vs #3990: those two codes are environment-state conflicts (the fix is a migration), not code bugs. Genuinely invalid declarations (unsupported operators etc.) must keep failing fast — the tolerance is scoped to 85/86 only.
Alternative considered
Running migrations before index builds would also unblock this case, but it inverts the #3990 invariant (unique-index idempotency guards exist before anything writes) and silently invalidates the ordering assumptions already codified in shipped migrations. Rejected in favor of the narrow tolerance above.
Regression test
A schema-declared index whose options differ from a pre-existing same-name index must not fail startup.