# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Quick Reference

- [Commands](#common-development-commands) | [Architecture](#code-architecture) | [Guidelines](#development-guidelines) | [Troubleshooting](#debugging--troubleshooting)
- **Detailed Rules**: See `.claude/rules/` for in-depth documentation

## Self-Improvement Instructions

**Claude Code must actively maintain and improve this file and `.claude/rules/`.**

After significant development sessions, update with new learnings (patterns, pitfalls, best practices). Keep this file concise (~150 lines) and move detailed topics to `.claude/rules/`.

## Living Documentation

The following documents must be kept up to date when making changes that affect them:

| Document | Update when... |
|----------|---------------|
| `docs/REQUEST-LIFECYCLE.md` | Adding/changing decorators, interceptors, guards, pipes, middleware, plugins, services, modules, or the request/response flow |
| `migration-guides/` | Releasing MINOR or MAJOR versions (see `.claude/rules/migration-guides.md`) |
| `.claude/rules/configurable-features.md` | Adding new configurable features |
| `FRAMEWORK-API.md` | Auto-regenerated by `pnpm run build` — verify after adding interfaces, CrudService methods, or core modules |

**Rule:** When a code change adds, removes, or modifies a feature listed in `docs/REQUEST-LIFECYCLE.md` (Features Overview, diagrams, decorator reference, configuration, etc.), update the document in the same commit or PR.

## Project Overview

**@lenne.tech/nest-server** - An extension layer on top of NestJS for building server applications with GraphQL and MongoDB.

- **NPM**: https://www.npmjs.com/package/@lenne.tech/nest-server
- **GitHub**: https://github.com/lenneTech/nest-server
- **Starter Project**: https://github.com/lenneTech/nest-server-starter (reference for migrations)
- **CLI**: https://github.com/lenneTech/cli (`lt server module <Name>`)
- **Package Manager**: pnpm (development only, published to npm registry)

### Consumption in downstream projects — npm vs vendor

Consumer projects (created via `lt fullstack init` / `lt server create`)
can consume this framework in one of two modes. As a framework author
working in THIS repo, you should understand both so your changes are
compatible with either downstream layout:

- **npm mode (classic)** — consumers install `@lenne.tech/nest-server`
  as an npm dependency. They read framework code from
  `node_modules/@lenne.tech/nest-server/` and import via the bare
  specifier `from '@lenne.tech/nest-server'`. This repo's `dist/` is
  what they actually see at runtime.

- **vendor mode (pilot)** — consumers copy this repo's `src/core/`
  (plus `src/index.ts`, `src/core.module.ts`, `src/test/`,
  `src/templates/`, `src/types/`, `LICENSE`, `bin/migrate.js`) directly
  into their project at `<api>/src/core/` as first-class project code.
  No npm dependency. Imports use relative paths. Consumers apply a
  **flatten-fix** that moves `src/index.ts` and `src/core.module.ts`
  under `src/core/` and rewrites `./core/…` → `./…`. Update flow is
  via the `lt-dev:nest-server-core-updater` Claude Code agent, which
  clones this repo at a target ref, computes a structured delta
  against the consumer's baseline, and presents a curated review.

**Implications for framework authors:**

- **Keep `src/core/` self-contained.** Any new import in `src/core/**`
  that points OUTSIDE `src/core/` (e.g. `../server/...`, `../main`)
  breaks vendor consumers, whose `src/server/` is unrelated to this
  repo's. The `src/server/` here is a framework-testing example, not
  a shipping component.

- **Keep top-level `src/index.ts` and `src/core.module.ts` consistent**
  with the core directory structure. The flatten-fix strips `./core/`
  from specifiers in these two files, so if you add re-exports there,
  use `export * from './core/common/...'` (not absolute paths).

- **Top-level meta files** like `FRAMEWORK-API.md`, `SHOWCASE.md`,
  `CHANGELOG.md`, and `load-tests/` are stripped from vendor consumer
  projects during `convertCloneToVendored`. They're framework-author-
  only content. `migration-guides/` is kept because the updater agent
  reads it at sync time — keep the migration guides readable outside
  of an npm-install context (no `node_modules/…` path references).

- **`package.json` devDeps** used at runtime by framework code (e.g.
  `find-file-up` in the config loader) must be listed in the CLI's
  `src/config/vendor-runtime-deps.json` as a runtime helper, otherwise
  vendor consumers miss them at install time. Prefer putting
  runtime-used packages in `dependencies` directly, not `devDependencies`.

- **Vendor-mode projects never run `pnpm update @lenne.tech/nest-server`.**
  They use `/lt-dev:backend:update-nest-server-core` instead, which
  does NOT bump a `package.json` entry but rewrites `src/core/**` in
  place. Do not assume version-bump telemetry from npm for vendored
  installs.

See the CLI's `convertCloneToVendored()` in
`lenneTech/cli/src/extensions/server.ts` for the full vendor
transformation, and the `lt-dev:nest-server-core-updater` agent for
the update-agent contract.

## Common Development Commands

```bash
# Building & Running
pnpm run build          # Build (outputs to dist/)
pnpm start              # Start in local mode
pnpm run start:dev      # Development mode with watch

# Testing (ALWAYS run before completing changes)
pnpm test               # Run E2E tests (Vitest)
pnpm run test:cov       # With coverage
pnpm run test:e2e-doh   # Debug open handles
pnpm run test:cleanup   # Remove leftover test artifacts (.txt, .bin)

# Linting & Formatting
pnpm run lint           # ESLint check
pnpm run lint:fix       # Auto-fix
pnpm run format         # Prettier format

# Package Development
pnpm run build:dev      # Build for local development (use with pnpm link)
pnpm run reinit         # Clean reinstall + tests + build
```

## Code Architecture

**Two-Layer Structure:**
- `src/core/` - Reusable framework components (exported)
- `src/server/` - Internal test implementation (not exported)
- `src/index.ts` - Public API exports

**Key Components:**
- `CoreModule` - Dynamic module with GraphQL (optional, disable via `graphQl: false`), MongoDB, security
- `src/core/common/` - Decorators, helpers, interceptors, services
- `src/core/modules/` - Auth, BetterAuth, ErrorCode, File, HealthCheck, Migrate, SystemSetup, Tus, User

See `.claude/rules/architecture.md` for detailed documentation.
See [`docs/REQUEST-LIFECYCLE.md`](docs/REQUEST-LIFECYCLE.md) for the complete request lifecycle, security architecture, and interceptor/decorator reference.

## Development Guidelines

### Core Principles

1. **Module Inheritance Pattern** - Extend through inheritance, not hooks/events
   - See `.claude/rules/module-inheritance.md`

2. **Dynamic Integration** - Components opt-in, configurable, no package modification required

3. **Backward Compatibility** - Changes affect all consuming projects

4. **Test Coverage** - All changes must pass `pnpm test`

5. **Export Management** - New public components → `src/index.ts`

### Role System (Critical)

System roles (`S_` prefix) are runtime checks only - **NEVER store in user.roles**:

```typescript
@Roles(RoleEnum.S_USER)      // Correct: runtime check
roles: [RoleEnum.S_USER]      // WRONG: never store S_ roles!
roles: [RoleEnum.ADMIN]       // Correct: real role
```

See `.claude/rules/role-system.md` for complete documentation.

### Versioning

`MAJOR.MINOR.PATCH` where MAJOR = NestJS version, MINOR = breaking changes, PATCH = non-breaking.

See `.claude/rules/versioning.md` for release process.

## Docker Production Build

The starter includes a multi-stage `Dockerfile` that works standalone and in monorepos:

```bash
docker build -t api .                                    # Standalone
docker build --build-arg API_DIR=projects/api -t api .   # Monorepo
```

Key files: `Dockerfile`, `docker-entrypoint.sh` (migrations + server start), `.dockerignore`

The migration store uses `NSC__MONGOOSE__URI` env var (not `config.env.ts`) for Docker compatibility.

## Environment Configuration

```typescript
// config.env.ts supports:
- Direct environment variables
- NEST_SERVER_CONFIG JSON variable
- NSC__* prefixed variables
```

**Key areas:** JWT, MongoDB, GraphQL, email, security, betterAuth

## Debugging & Troubleshooting

| Issue | Solution |
|-------|----------|
| Tests timeout | Ensure MongoDB running on localhost:27017 |
| GraphQL introspection fails | Check `config.env.ts` introspection setting |
| Module not found after adding | Verify export in `src/index.ts`, run `pnpm run build` |
| Open handles in tests | Run `pnpm run test:e2e-doh` |

## Best Practices

1. **All code, comments, documentation in English**
2. **Run tests before completing changes** - `pnpm test`
3. **Follow existing patterns** for consistency
4. **Never store S_ roles** in user.roles array
5. **Use Module Inheritance Pattern** for core modules
6. **Document breaking changes** in commits
7. **Integration Checklists for Core Modules** - Every core module requiring project integration needs `INTEGRATION-CHECKLIST.md` (see `.claude/rules/core-modules.md`)
8. **Don't add redundant @UseGuards** - `@Roles()` already handles JWT auth (see `.claude/rules/role-system.md`)
9. **Fixed package versions only** - Never use `^`, `~`, or ranges in package.json (see `.claude/rules/package-management.md`)

## Migration Guides

When releasing MINOR or MAJOR versions, create migration guides in `migration-guides/`:
- Use `migration-guides/TEMPLATE.md` as starting point
- Always analyze `src/server/` and [nest-server-starter](https://github.com/lenneTech/nest-server-starter)
- Ask developer for additional projects to analyze
- See `.claude/rules/migration-guides.md` for complete process

## Modular Rules

Detailed documentation in `.claude/rules/`:

| File | Content |
|------|---------|
| `module-inheritance.md` | Core architectural pattern for extending modules |
| `role-system.md` | Role system, S_ prefix rules, @Roles vs @UseGuards |
| `architecture.md` | Detailed code architecture |
| `testing.md` | Test configuration and best practices |
| `versioning.md` | Version strategy and release process |
| `core-modules.md` | Path-scoped rules for `src/core/modules/` incl. Integration Checklist requirements |
| `better-auth.md` | **Security-critical**: Better-Auth module rules (standard compliance, security, testing) |
| `module-deprecation.md` | Legacy Auth → BetterAuth migration roadmap |
| `migration-guides.md` | Process for creating version migration guides |
| `configurable-features.md` | Configuration patterns: "Presence implies enabled" and "Boolean shorthand" (`true` / `{}`) |
| `package-management.md` | Fixed package versions only - no `^`, `~`, or ranges |
| `framework-compatibility.md` | Maintenance obligations for FRAMEWORK-API.md, shipped docs, cross-repo dependencies |

## Native MongoDB Driver — Security Rules

### model.collection / model.db — BLOCKED

Access to `this.mainDbModel.collection` and `this.mainDbModel.db` is blocked via TypeScript type (`SafeModel = Omit<Model, 'collection' | 'db'>`).
For additionally injected models (`@InjectModel`): **NEVER** use `.collection.*` or `.db.*`.
All Mongoose plugins (Tenant, Audit, RoleGuard, Password) are bypassed.

**Use Mongoose Model methods instead:**

| Forbidden | Allowed |
|-----------|---------|
| `collection.insertOne(doc)` | `Model.insertMany([doc])` |
| `collection.bulkWrite(ops)` | `Model.bulkWrite(ops)` |
| `collection.updateOne(f, u)` | `Model.updateOne(f, u)` |
| `collection.updateMany(f, u)` | `Model.updateMany(f, u)` |
| `collection.deleteOne(f)` | `Model.deleteOne(f)` |
| `collection.deleteMany(f)` | `Model.deleteMany(f)` |
| `collection.find(f)` | `Model.find(f)` or `Model.find(f).lean()` |
| `collection.aggregate(p)` | `Model.aggregate(p)` |

If native access is unavoidable: use `this.getNativeCollection(reason)` or `this.getNativeConnection(reason)` from CrudService.

### connection.db.collection() — Use With Caution

Allowed for:
- Collections without Mongoose schema (OAuth, BetterAuth, MCP)
- Read-only aggregations/counts on other collections
- Admin operations (indexes, drops, backups)

**NOT allowed for:**
- Write operations on tenant-scoped collections → use Mongoose Model instead
- CRUD on collections that have a Mongoose schema → use CrudService instead

Details: [`docs/native-driver-security.md`](docs/native-driver-security.md)

### CrudService process() Pipeline — Memory Considerations

The `process()` pipeline (prepareInput → checkRights → serviceFunc → processFieldSelection → prepareOutput → checkRights) adds memory overhead per call through object cloning, Mongoose hydration, and populate operations. For typical API usage this is negligible, but it can become significant in:

- **High-frequency operations** (e.g. monitor checks running every 10-60 seconds per monitor)
- **Service cascades** (Service A → Service B → Service C, each going through process())
- **Populate chains** (3-5 levels of nested population)

### When to Use CrudService vs Direct Mongoose

`CrudService.create/update/get()` wraps every operation in `process()` which provides:
- **Input validation** (prepareInput: type mapping, ObjectId conversion)
- **Authorization** (checkRights: `@Restricted`, `S_CREATOR`, `S_SELF`, field-level permissions)
- **Output filtering** (prepareOutput: recursive secret removal, recursive ObjectId→string conversion, strip restricted fields)

**Use CrudService** when the caller is a **user-facing API** (Controller → Service → Response to User) — authorization and field filtering are required.

**Use direct Mongoose** when the caller is **system-internal** (Processor, Cron, Service-to-Service) — no user context exists, no response is sent to a user, and input is internally controlled.

| Context | Use | Why |
|---------|-----|-----|
| Controller handling user request | `CrudService.update()` | User needs authorization + field filtering |
| Cron job updating internal state | `Model.findByIdAndUpdate()` | No user context, no response to user |
| Queue processor writing results | `Model.create()` | System-internal, plugins provide tenant/audit |
| WebSocket emit loading data | `Model.findById().lean()` | Read-only, clients receive JSON |
| Appending to subdoc array | `CrudService.pushToArray()` | Always — even in controllers (see rule 6 below) |
| Removing from subdoc array | `CrudService.pullFromArray()` | Atomic removal, bypasses process() |

### Performance Alternatives (System-Internal Only)

These alternatives bypass `process()` but keep Mongoose plugins (Tenant, Audit, RoleGuard) active. **Only use in system-internal contexts** where no user authorization or output filtering is needed.

| Instead of | Use | Relative Speed | Relative Memory |
|-----------|-----|---------------|-----------------|
| `CrudService.create(input)` | `Model.create(input)` | ~3x faster than `save()` | ~9x less than `save()` |
| `CrudService.create(input)` (batch N) | `Model.insertMany(docs)` | ~2.5x faster than N× `save()` | ~3x less than N× `save()` |
| `CrudService.update(id, input)` | `Model.findByIdAndUpdate(id, input).lean()` | Fastest update pattern | Lowest memory |
| `CrudService.get(id)` / `getForce(id)` | `Model.findById(id).lean()` | ~2x slower per call | ~5x less memory |

> **Note:** `Model.create()` is significantly faster and lighter than `new Model().save()` for single documents.
> `findById().lean()` trades per-call speed for much lower memory — prefer lean in high-frequency paths where memory stability matters more than per-call latency. For low-frequency user-facing paths, hydrated reads via CrudService are preferred.
> `Model.create()` internally calls `save()` but is optimized by Mongoose — it triggers all `pre('save')` hooks (Tenant, Audit, RoleGuard, Password).

**NEVER** bypass Mongoose entirely via `collection.*` — see section above. The CheckSecurityInterceptor acts as a safety net on HTTP responses regardless of how data was written.

> **Secret removal happens in two layers:**
> - **`prepareOutput`** (service-level, in `process()`): Recursively removes `password`, `verificationToken`, `passwordResetToken` — fields that are NEVER needed internally after hashing/use.
> - **`CheckSecurityInterceptor`** (HTTP-level, Safety Net): Removes the full configurable `security.secretFields` list (additionally `refreshTokens`, `tempTokens`) — these are needed internally for token validation and process flows but must not reach HTTP clients.

### High-Frequency Path Design Rules

These rules apply when building features that execute many times per minute (monitoring, metrics, queue processors). These paths are always system-internal — no user context, no authorization needed.

1. **`Model.create(doc)` over `new Model().save()`** for single documents — `create()` is ~3x faster and uses ~9x less memory. For batch inserts (N>1), use `Model.insertMany(docs)` — a single call with N documents is ~2.5x faster than N parallel `save()` calls.

2. **`Model.findById().lean()` over `getForce()`** for read-only lookups — lean uses ~5x less memory but is ~2x slower per call. In hot paths, the memory savings outweigh the latency cost because reduced GC pressure improves overall throughput. For low-frequency user-facing paths, hydrated reads via CrudService are preferred (faster + authorization).

3. **Defer complex logic to cron/queue** — high-frequency paths should only write data (checks, metrics). Processing that data (incident creation, notifications, escalation) belongs in low-frequency cron jobs or separate queue processors that run in bounded batches.

4. **Avoid service cascades in hot paths** — if Service A calls Service B which calls Service C, each with `process()`, the objects from all three levels live in the heap simultaneously. In hot paths, call Mongoose directly instead of going through other services.

5. **WebSocket emits should use lean data** — `emitUpdated()` with `getForce()` triggers a full `process()` pipeline per emit. Use `Model.findById().lean()` instead — WebSocket clients receive JSON, not Mongoose Documents.

6. **NEVER pass Mongoose SubDocument Arrays through `CrudService.update()`** — this applies in ALL contexts (user-facing AND system-internal). Mongoose subdocument arrays (e.g. `entity.logs`, `entity.comments`, `entity.history`) are Proxy-wrapped objects. When passed through `process()`, `clone()` (rfdc) and `processDeep()` trigger Proxy getters/setters on every property of every subdocument, and `mergePlain()` clones the entire array again. On long-lived documents with hundreds of entries this causes OOM (3GB+ heap). Use `CrudService.pushToArray()` / `pullFromArray()` or direct `$push` / `$set`:
   ```typescript
   // WRONG — even in a controller, this causes OOM on large arrays
   entity.logs.push(newLog);
   await this.entityService.update(id, { logs: entity.logs });

   // CORRECT — use CrudService helper (preferred)
   await this.entityService.pushToArray(id, 'logs', newLog);
   await this.entityService.pushToArray(id, 'logs', newLog, { $slice: -500 }); // cap at 500

   // CORRECT — direct Mongoose for combined operations
   await entityModel.findByIdAndUpdate(id, {
     $push: { logs: newLog },
     $set: { status: 'RESOLVED', resolvedAt: new Date() },
   }).lean().exec();

   // CORRECT — remove items by condition
   await this.entityService.pullFromArray(id, 'tags', 'obsolete');
   ```

7. **Clear timers in Promise.race patterns** — if using `Promise.race([operation, timeout])`, always clear the timeout after the race resolves. Leaked timers accumulate memory and cause unhandled rejections in high-frequency paths.

8. **In-memory buffers need eviction** — any `Map` or array used as a buffer (error tracking, dedup, cache) must have a size cap and periodic cleanup. Without eviction, buffers grow unbounded and are never GC'd.

9. **Queue-based crons must prevent re-enqueue of in-progress items** — if a cron adds jobs to a BullMQ queue faster than the processor consumes them, the queue grows unbounded. Guard with `getWaitingCount()` check before enqueuing.

Details: [`docs/process-performance-optimization.md`](docs/process-performance-optimization.md)
Details: [`docs/subdocument-array-optimization-plan.md`](docs/subdocument-array-optimization-plan.md)

## Mongoose Index Placement

**Rule:** Single-field indexes live on the property. `Schema.index()` is reserved for compound (multi-field) indexes only.

1. **Single-field indexes** → declare directly on the property via `@Prop({ index: true })` or `@UnifiedField({ mongoose: { index: true } })`. Keeps all property info in one place and visible during code review.

2. **Framework-managed indexes** → do NOT set manually. Example: `tenantId` is automatically indexed by the `mongooseTenantPlugin` (`src/core/common/plugins/mongoose-tenant.plugin.ts` → `schema.index({ tenantId: 1 })`). Adding `index: true` on `tenantId` in a consumer project triggers Mongoose `"Duplicate schema index"` warnings. Any future plugin that auto-indexes a field must be documented here.

3. **Compound (multi-field) indexes** → only these belong in `Schema.index({ field1: 1, field2: 1 })` after `SchemaFactory.createForClass(...)`. This is the only acceptable use of `Schema.index()`.

4. **TTL indexes** → declare inline: `@Prop({ required: true, index: { expireAfterSeconds: 3600 } })`. Do NOT duplicate with a separate `Schema.index()` call — the single-field TTL belongs on the property.

5. **`unique: true` implies an index** → Mongoose auto-creates a unique index for `@Prop({ unique: true })`. Do not additionally declare `Schema.index({ field: 1 }, { unique: true })`.

**Why:** Property-local indexes are immediately visible when reviewing a field. Hidden `Schema.index()` calls at the bottom of the file are easy to miss, leading to duplicate definitions and Mongoose warnings when multiple developers touch the same model. When building plugins that auto-index fields, always emit a warning in the plugin's docstring so consumers know not to set the index manually.

## In-Depth Documentation

| File | Content |
|------|---------|
| [`docs/REQUEST-LIFECYCLE.md`](docs/REQUEST-LIFECYCLE.md) | Complete request lifecycle, security architecture, interceptor chain, decorator reference, CrudService pipeline, Safety Net, diagrams |
| [`docs/native-driver-security.md`](docs/native-driver-security.md) | Native MongoDB Driver restrictions, secure alternatives, review checklist |
| [`docs/process-performance-optimization.md`](docs/process-performance-optimization.md) | process() pipeline performance optimizations |
| [`docs/subdocument-array-optimization-plan.md`](docs/subdocument-array-optimization-plan.md) | SubDocument array handling (implemented): pushToArray/pullFromArray, checkRestricted optimization, bug fixes |
