add-invoices-tenant-indexrun.md00_intake/stub.mdThe db:audit CI run flagged [missing-tenant-index] invoices: Documents carry "tenantId" but no index leads with it — tenant-scoped queries scan the collection. The tenantPlugin
(packages/services/src/db/plugins/tenant.ts) injects tenantId into every find / findOne / update /
delete / count query, so every read of invoices is tenant-scoped. But none of the collection's
indexes lead with tenantId — they lead with milestone, status, expert, customer, lead
(packages/services/src/db/models/invoice.ts:55-59). The standalone tenantId index the plugin would
add only fires when the schema doesn't already declare tenantId; invoice.ts declares it
explicitly, so no tenant-leading index exists. Result: tenant-scoped invoice queries can't seek on
tenantId and degrade toward collection scans as data grows. This is the audit's number-one priority
finding by impact ÷ risk: a core financial collection, certain performance win, additive low-risk fix.
Add tenant-leading index coverage to invoices, shipped as a reviewed forward/back migration
(pnpm --filter @sustentus/services db:migrate create add-invoices-tenant-index) — never ad hoc.
Prefer folding tenantId into the existing compound indexes following ESR (Equality, Sort, Range)
rather than adding a bare { tenantId: 1 }, so the real query shapes are covered:
{ tenantId: 1, status: 1, isApproved: 1 }{ tenantId: 1, expert: 1, status: 1 }{ tenantId: 1, customer: 1, status: 1 }{ tenantId: 1, lead: 1, status: 1 }Define/Build confirm the exact set against actual query usage (and whether the now-redundant
non-tenant-leading compounds should be dropped in the same migration). The { milestone: 1 } unique
index stays as-is.
up/down adds tenant-leading index(es) to invoices and drops any
index it makes redundant; down cleanly reverses it.invoice.ts schema index(...) declarations match the migration (schema and live DB agree).db:audit no longer reports missing-tenant-index for invoices.missing-tenant-index collections (csats, milestones, statushistories).unused-index, redundant-index, or orphaned-reference finding (incl. the 2/19 orphaned
invoice docs) — separate stubs / decomposition.db-auditor + mongodb-query-optimizer skills, ESR guidance).packages/services/src/db/models/invoice.ts, a new migration under packages/services.01_define/output/spec.mdThe db:audit CI run flags its number-one finding by impact ÷ risk:
[missing-tenant-index] invoices: Documents carry "tenantId" but no index leads with it — tenant-scoped queries scan the collection.
The tenantPlugin (packages/services/src/db/plugins/tenant.ts) injects tenantId into every
find / findOne / update / delete / count on invoices, so every read is tenant-scoped. But the
plugin only adds its own { tenantId: 1 } index when the schema doesn't already declare tenantId —
and invoice.ts:34 declares it explicitly, so that fallback never fires. The collection's five
indexes lead with milestone, status, expert, customer, lead (invoice.ts:55–59); none lead
with tenantId. Tenant-scoped invoice queries therefore can't seek on tenantId and degrade toward
collection scans as data grows. This advances the database leanness & query efficiency initiative
(objective: keep tenant-scoped queries index-backed) on a core financial collection — a certain,
additive, low-risk performance win.
Add tenant-leading index coverage to invoices, shipped as a reviewed forward/back migration created
with pnpm --filter @sustentus/services db:migrate create add-invoices-tenant-index — never ad hoc.
Following ESR (Equality, Sort, Range), fold tenantId into the existing compound indexes as the
leading equality field rather than adding a bare { tenantId: 1 }, so the real tenant-scoped query
shapes are covered:
{ tenantId: 1, status: 1, isApproved: 1 }{ tenantId: 1, expert: 1, status: 1 }{ tenantId: 1, customer: 1, status: 1 }{ tenantId: 1, lead: 1, status: 1 }The { milestone: 1 } unique index stays as-is (its uniqueness is global, not tenant-scoped). Build
confirms the exact set against actual invoice query usage in the codebase before writing the
migration, and decides — read first, drop second — whether the now-redundant non-tenant-leading
compounds ({ status, isApproved }, { expert, status }, { customer, status }, { lead, status })
should be dropped in the same migration, verifying nothing relies on them. The invoice.ts schema
index(...) declarations are updated to match the migration so schema and live DB agree.
up/down adds the tenant-leading index(es) to invoices and drops
any index it makes redundant; down cleanly reverses it (re-creating dropped indexes, removing
added ones).invoice.ts schema index(...) declarations match the migration exactly, so the schema and
the live DB agree.db:audit no longer reports missing-tenant-index for invoices.missing-tenant-index collections (csats, milestones, statushistories) —
separate stubs.unused-index, redundant-index, or orphaned-reference finding (including the orphaned
invoice docs) — separate stubs / decomposition.tenantId, ESR, drop only what is provably redundant —
and confirmed by Build against the code; neither blocks an acceptance criterion.)02_build/output/notes.mdfeat: add-invoices-tenant-index — tenant-leading invoice indexes + migrationpackages/services/src/db/migrations/1782200000000-add-invoices-tenant-index.ts (new): forward/back
migration. up creates four tenant-leading indexes and drops the four superseded
non-tenant-leading compounds; down reverses it exactly. Operates through the raw
connection.collection("invoices") per the migration convention, with index drops tolerant of an
already-absent index so a partial re-run converges.packages/services/src/db/models/invoice.ts: replaced the four non-tenant-leading compound
index(...) declarations with the four tenant-leading ones, so the schema matches the migration. The
{ milestone: 1 } unique index is unchanged.Read the actual invoice queries in db/services/invoice/index.ts first. The dominant tenant-scoped
shapes are paginated lists sorted by createdAt, plus lead-detail queries — so ESR puts createdAt
as the trailing Sort field (not the stub's draft isApproved, which no query filters on).
| Index | Serves |
|---|---|
{ tenantId, createdAt: -1 } |
findAll (tenant invoice list, sort createdAt desc) + countDocuments({ tenantId }) via the tenantId prefix |
{ tenantId, customer, createdAt: -1 } |
findAll role=customer; the payable-on-lead query (customer in equality) |
{ tenantId, expert, createdAt: -1 } |
findAll role=expert |
{ tenantId, lead, status, createdAt: -1 } |
findDraftInvoicesForLeadAndManager + findPayableInvoicesForLeadAndCustomer (E = tenantId, lead, status; S = createdAt) |
Dropped (superseded; redundant once every query is tenant-scoped — no query filters on these leading
fields without tenantId): { status, isApproved }, { expert, status }, { customer, status },
{ lead, status }.
Deliberately not added (avoiding over-indexing — the audit's whole point is leanness):
{ tenantId, vendor, createdAt } — vendor/manager are oversight roles that reach invoices via
lead/request detail (covered by the lead index) rather than a flat per-vendor list; the rare
vendor-filtered findAll falls back to { tenantId, createdAt } + a residual filter.{ tenantId, quote } for archiveByQuoteIds — a rare admin/cascade op, served acceptably by the
tenantId prefix.{ tenantId, status, isApproved } (the stub's draft shape) — no query filters on status/isApproved
at tenant level without a more selective field, so it would be speculative.No new index is a prefix of another (checked against the audit's redundant-index rule), and each
leads with tenantId (clears missing-tenant-index).
up/down adds tenant-leading index(es) and drops the redundant
compounds; down cleanly reverses it — 1782200000000-add-invoices-tenant-index.ts.invoice.ts index(...) declarations match the migration exactly (indexes inherit the
collection's default collation in both paths, so identities agree).db:audit no longer reports missing-tenant-index for invoices — four indexes now
lead with tenantId, which is exactly the condition the audit checks (keyFields(index)[0] === "tenantId").Audit database and Migrate preview database
CI jobs additionally exercise this change against a live preview DB.down is a true reversal (not a no-op): it restores the original compounds and removes the new ones,
matching the established index-migration symmetry rule.03_release/output/investor-update.mdWho it's for: Every persona that views invoices (platform-wide) What shipped: Invoice lookups are now backed by tenant-leading database indexes instead of scanning the collection. Why it matters: Keeps payout-flow infrastructure reliable at volume — advancing Scale the Bridge and our Q2 goal to validate technical infrastructure.
Dig deeper: <merged-PR URL>
03_release/output/release.mdup creates new indexes before
dropping old (no read gap); dropIfExists swallows only IndexNotFound and rethrows otherwise;
down is an exact symmetric reversal; schema and migration index identities agree (both inherit the
collection's default collation); CONVENTIONS satisfied (arrow fns, type, async/await, named imports).up/down adds tenant-leading index(es) and drops the redundant compounds; down reverses it — 1782200000000-add-invoices-tenant-index.tsinvoice.ts index(...) declarations match the migration exactlydb:audit no longer reports missing-tenant-index for invoices — four indexes lead with tenantId; the Audit database CI job is green