Skip to content

🐛 Boot: same-name index conflict crashes startup before the migration that repairs it can run #4004

Description

@PierreBrisorgueil

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions