# Pathrule Pattern: MongoDB + Mongoose (1.0.0)
# ::pathrule:package:mongodb-mongoose

### [RULE] Declare an index for every query shape you ship  (path: /src/models)
<!-- scope: folder | priority: high | strict -->

MongoDB will happily scan a collection forever. The query works in development on a thousand documents and takes seconds on a million.

- Every field combination you filter or sort on needs a compound index in the right order: equality fields first, then the sort field, then range fields. An index on the wrong order does not serve the query.
- Declare indexes in the schema (or a migration) and turn `autoIndex` off in production. Automatic index builds on startup are a surprise write load on a live cluster, and on a large collection they block.
- Back every uniqueness rule with a unique index. A Mongoose `unique: true` is only an index hint, and a validator alone loses the race between two concurrent writes.
- Use partial or sparse indexes for fields present on a subset of documents, and TTL indexes for data that should expire, rather than a cron job that deletes rows.
- Verify with `explain("executionStats")` that a new query uses an index (`IXSCAN`, not `COLLSCAN`) and that the examined-to-returned ratio is near one. Do not add indexes by feel: each one costs write throughput and memory.

---

### [RULE] Read lean with projections, write with targeted operators  (path: /src/db)
<!-- scope: folder | priority: high | strict -->

Mongoose hydrates every result into a full document with getters, virtuals, and change tracking. For a read-only endpoint that is pure overhead.

- Add `.lean()` to any query whose result is serialized and returned. It returns plain objects and is dramatically cheaper on large result sets. Keep hydration for paths that call document methods or save.
- Project the fields you need (`.select("name email")`). Returning whole documents is how a password hash or an internal flag reaches a client.
- Update with atomic operators (`$set`, `$inc`, `$push` with `$slice`, `findOneAndUpdate`) rather than loading a document, mutating it, and saving. Read-modify-write loses concurrent updates.
- Bound every list: a filter plus a limit, paginated by an indexed cursor field (`_id` or a timestamp) rather than a growing `skip`.
- Never use `$where` or pass raw user input into a query object without validating its shape; an object where a string is expected turns into an operator injection.

---

### [MEMORY] Embed or reference: decide from the read pattern  (path: /src/models)

This is the only schema decision that really matters in MongoDB, and it is a query decision, not a modelling-purity decision.

- Embed when the child is always read with the parent, is bounded in count, and changes with it (an address on a user, line items on an order). One read, no join.
- Reference when the set is unbounded (events, messages, audit entries), when it is shared by many parents, or when it is queried independently. Then the child collection carries the parent id and its own indexes.
- Never let an array grow without bound inside a document. Documents have a hard size limit, and long arrays make every update rewrite the document and every index entry.
- Duplicating a few fields for read speed (a denormalised author name next to a post) is normal here, as long as you decide who updates the copy and accept that it is eventually consistent.
- `$lookup` exists but is not a join engine: on a hot path, prefer a shape that answers the query in one collection, or fetch by ids in a second query and join in memory.

See /src/models for the index rule and /src/db for the query rules that assume this shape.

---

### [MEMORY] Transactions are the exception, single-document atomicity is the rule  (path: /src/db)

MongoDB supports multi-document ACID transactions, and reaching for them constantly is a sign the schema is fighting the database.

- A single document update is atomic on its own, including nested fields and arrays. Shape the document so the invariant you care about lives in one document, and you need no transaction at all.
- When you do need one (money moving between two accounts, a two-collection invariant), keep it tiny: no network calls, no user input waiting, ideally sub-second. The server aborts long-running transactions, and open transactions hold resources and block other writers.
- Wrap transactions in retry logic for `TransientTransactionError` and `UnknownTransactionCommitResult`. A transaction that is not retried is a transaction that fails under load.
- Transactions require a replica set (or sharded cluster). Code that assumes them will fail against a standalone development instance, so run a single-node replica set locally.
- Set read and write concern deliberately: `majority` for anything you must not lose or read stale, weaker only where you have decided the trade-off.

See /src/models for the embedding decision that removes most transactions.

---

### [MEMORY] Connection lifecycle, especially in serverless  (path: /src/db)

The driver manages a connection pool. Most MongoDB outages in serverless apps are self-inflicted pool exhaustion.

- Call `mongoose.connect` (or create the client) once per process, in a module that exports the promise, and await that promise in handlers. Never connect per request.
- In serverless, cache the connection on `globalThis` so warm invocations reuse it, and keep `maxPoolSize` small (a handful of sockets per instance): concurrency there is the number of instances, not the number of requests. Never call `disconnect` at the end of a handler.
- Set sensible timeouts (`serverSelectionTimeoutMS`, `socketTimeoutMS`) so a network problem fails fast instead of hanging every request behind a full pool.
- Handle connection events for observability (`connected`, `error`, `disconnected`) and expose pool metrics; a rising checkout wait time is the earliest signal of trouble.
- Register models once. Re-registering the same model name on hot reload throws, which is why the connection module and the model registry belong together.

See /src/db for the query rules that run on this pool, and the redis-caching pattern if reads need a cache in front.

---

### [SKILL] mongodb-query-review  (path: /)

---
name: mongodb-query-review
description: MongoDB and Mongoose review checklist covering index coverage, lean reads, document growth, transaction scope, and connection reuse. Run before merging a schema or query change.
---

# MongoDB review

## Indexes
- [ ] Every new filter and sort is covered by an index, in equality, sort, range order.
- [ ] `explain("executionStats")` shows an index scan and a sane examined-to-returned ratio.
- [ ] Uniqueness is enforced by a unique index, not only a validator.
- [ ] `autoIndex` is off in production; the index is built deliberately.

## Queries
- [ ] Read-only queries use `.lean()` and a projection.
- [ ] Updates use atomic operators, not read-modify-write.
- [ ] Every list is bounded and paginated on an indexed cursor field.
- [ ] No `$where`; user input cannot become an operator.

## Document design
- [ ] Embedded data is read with its parent and bounded in size.
- [ ] No array can grow without limit.
- [ ] Denormalised copies have a named owner that updates them.

## Transactions and connections
- [ ] The invariant genuinely spans documents; otherwise no transaction.
- [ ] Transactions are short, contain no network calls, and retry on transient errors.
- [ ] The connection is created once per process and reused across invocations.
