# Monolith Package Express.js API with Sequelize + PostgreSQL, TypeDI for dependency injection, class-validator for validation, gRPC support, AWS services (S3, SQS, SNS), Redis caching. ## Architecture: Two Coexisting Patterns **New code must always use the hexagonal pattern.** ### Old (Legacy) — do NOT use for new code Flat structure under `src/api/`: `controllers/`, `services/`, `models/` (model IS the Sequelize DAO). No ports/adapters, no dependency inversion. ~37 legacy controllers, ~48 legacy services still exist. ### New (Hexagonal) — required for all new code ``` src/api/modules/{moduleName}/ ├── presentation/ # Controllers, routers, VCs, SCs, DTOs ├── business/ # Services, ports, domain models ├── infrastructure/ # Adapters, DAOs, mappers └── {moduleName}PortsDI.ts # Port-to-adapter DI mapping ``` Dependency direction: `Presentation → Business → Infrastructure` ### Hexagonal Layer Rules These rules are enforced by architecture tests in `test/architecture/` and must be followed when writing or modifying any hexagonal module. | Rule | What it means | Allowed imports within the module | |------|--------------|-----------------------------------| | **Business is the innermost layer** | `business/` must never import from `infrastructure/` or `presentation/` | Only from `business/` itself | | **Infrastructure depends on business only** | `infrastructure/` must never import from `presentation/` | From `business/` and `infrastructure/` itself | | **Presentation depends on business only** | `presentation/` must never import from `infrastructure/` | From `business/` and `presentation/` itself | | **Infrastructure is encapsulated** | Only `{moduleName}PortsDI.ts` can import from `infrastructure/` | No other file should reference DAOs, adapters, or mappers directly | | **Module boundary** | External code must not reach into a module's internal layers | Only exported services are the module's public interface | When modifying a hexagonal module: - Never import a DAO, adapter, or mapper outside of `infrastructure/` or `PortsDI.ts` - Never import a VC, SC, or controller from `business/` or `infrastructure/` - If you need infrastructure data from business layer, define a port in `business/ports/` and implement an adapter in `infrastructure/adapters/` - External modules that need to interact with this module should consume its services via DI (typically through an adapter in their own module) — never reach into internal layers ## Naming Conventions | Artifact | Convention | Example | |----------|-----------|---------| | Module folder | camelCase | `segmentations`, `timeTracking` | | Domain model | PascalCase | `SegmentationGroup` | | ForCreation class | PascalCase + `ForCreation` | `SegmentationGroupForCreation` | | DAO | PascalCase + `DAO` suffix | `SegmentationGroupDAO` | | Port | PascalCase + `Port` suffix | `SegmentationsRepositoryPort` | | Adapter | PascalCase + `Adapter` suffix | `SegmentationsRepositoryAdapter` | | Service | PascalCase + `Service` suffix | `SegmentationsService` | | Controller | PascalCase + `Controller` suffix | `SegmentationsController` | | Validation class | PascalCase + `VC` suffix | `CreateSegmentationGroupVC` | | Serialization class | PascalCase + `SC` suffix | `SegmentationGroupSC` | | DTO interface | PascalCase + `DTO` suffix | `SegmentationGroupDTO` | | Mapper function | `{dao}To{Domain}` / `{domain}To{Dao}` | `segmentationGroupDAOToSegmentationGroup` | | PortsDI file | `{moduleName}PortsDI.ts` | `segmentationsPortsDI.ts` | | Router export | `start{Module}{Context}Router` | `startSegmentationsAppRouter` | ## Domain Models: Two Coexisting Patterns **New code must always use the ForCreation pattern.** ### New (ForCreation) — required for all new code ```typescript import { OptionalNullable } from '@humand-packages/common'; export class MyEntityForCreation { constructor(properties: OptionalNullable) { this.instanceId = properties.instanceId; this.name = properties.name; this.description = properties.description ?? null; } readonly instanceId: number; public name: string; public description: string | null; } export class MyEntity extends MyEntityForCreation { constructor(properties: OptionalNullable) { super(properties); this.id = properties.id; this.createdAt = properties.createdAt; this.updatedAt = properties.updatedAt; this.deletedAt = properties.deletedAt ?? null; } readonly id: number; readonly createdAt: Date; readonly updatedAt: Date; public deletedAt: Date | null; update(updateParams: { name?: string; description?: string | null }) { this.name = updateParams.name ?? this.name; this.description = updateParams.description !== undefined ? updateParams.description : this.description; } } ``` Rules: - `ForCreation` holds fields needed to create a new entity - Entity extends `ForCreation`, adds `id`, `createdAt`, `updatedAt` - Constructor receives `OptionalNullable` (nullable props become optional) - `readonly` for immutable fields, `public` for mutable fields - Business logic (update, validation) lives on the class itself - No getters/setters — direct property access ### Old (Getter/Setter) — legacy, do NOT use Private fields with fluent arrow-function getters/setters returning `this`. Still present in many modules. ## Key Patterns | Pattern | Convention | Location | |---------|-----------|----------| | Controller | `extends BaseController`, `@Service()` | `presentation/controllers/` | | Service | `@Service()`, `@PortsDI(Port)` for injection | `business/services/` | | Port | `abstract class extends BaseRepositoryPort` | `business/ports/` | | Adapter | `extends BaseRepositoryAdapter implements Port`, `@Service()` | `infrastructure/adapters/` | | DAO | `@Table`, `@Column`, `@Scopes` (sequelize-typescript) | `infrastructure/dataAccessObjects/` | | Mapper | Pure functions: `entityDAOToEntity` / `entityForCreationToDAO` | `infrastructure/mappers/` | | Validation | `extends ValidationClass`, class-validator + `@Expose()` | `presentation/validationClasses/` | | Serialization | `extends SerializationClass`, `serialize()` | `presentation/serializationClasses/` | | DI Registration | `{ [Port.name]: Adapter }` exported object | `{module}PortsDI.ts` | ## Controller ```typescript @Service() export class MyController extends BaseController { constructor( private readonly myService: MyService, private readonly queuePort: DeferredExecutionQueuePort, @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.NOTIFICATION_DELAY) notificationDelay: number, logger: LoggerPort, ) { super(logger, notificationDelay); } public create = async (req: Request, res: MyResponse, next: NextFunction): Promise => { const { bodyParams, loggedUser } = res.locals; const params = bodyParams as CreateMyEntityVC; await this.endpointHandlerWithGenerics( next, () => getSequelize().transaction(async () => { const entity = await this.myService.create(loggedUser.getInstanceId(), params); res.status(CREATED).json(new MyEntitySC().serialize(entity)); return { entity }; }), async ({ entity }) => { await this.queuePort.sendMessage(QUEUES.EVENT_HANDLER, QueueMessageTypes.MY_EVENT, { instanceId: loggedUser.getInstanceId(), metadata: { id: entity.id }, }); }, ); }; } ``` `endpointHandlerWithGenerics` takes: `next`, a transactional function (returns data), and an optional post-transaction async callback (for queue messages, notifications). ## Service ```typescript @Service() export class MyService { constructor( @PortsDI(MyRepositoryPort) private readonly repository: MyRepositoryPort, ) {} public create = async (instanceId: number, data: CreateMyEntityVC): Promise => { const entity = new MyEntityForCreation({ instanceId, name: data.name, description: null }); return await this.repository.create(entity); }; } ``` ## Port, Adapter, Mapper ```typescript export abstract class MyRepositoryPort extends BaseRepositoryPort { abstract getById: (id: number) => Promise; abstract create: (entity: MyEntityForCreation) => Promise; } @Service() export class MyRepositoryAdapter extends BaseRepositoryAdapter implements MyRepositoryPort { public getById = async (id: number): Promise => { const dao = await MyEntityDAO.findByPk(id); if (!dao) throw new NotFoundError('Not found', ErrorCodes.NOT_FOUND); return myEntityDAOToMyEntity(dao); }; public create = async (entity: MyEntityForCreation): Promise => { const dao = myEntityForCreationToDAO(entity); await dao.save(); return myEntityDAOToMyEntity(dao); }; } export const myEntityDAOToMyEntity = (dao: MyEntityDAO): MyEntity => { return new MyEntity({ id: dao.id, name: dao.name, description: dao.description, instanceId: dao.instanceId, createdAt: dao.createdAt, updatedAt: dao.updatedAt, deletedAt: dao.deletedAt ?? null, }); }; export const myEntityForCreationToDAO = (entity: MyEntityForCreation): MyEntityDAO => { return MyEntityDAO.build({ name: entity.name, description: entity.description, instanceId: entity.instanceId, }); }; ``` ## DAO ```typescript @Scopes(() => ({ ofInstance(instanceId: number) { return { where: { instanceId } }; }, })) @Table({ tableName: 'MyEntities' }) export class MyEntityDAO extends Model { id!: number; @Column name!: string; @Column instanceId!: number; @CreatedAt @Column createdAt!: Date; @UpdatedAt @Column updatedAt!: Date; } export interface MyEntityCreationFields { id?: number; name: string; instanceId: number; } ``` Create `infrastructure/dataAccessObjects/all.ts` to export DAOs: ```typescript export const myModuleDataAccessObjects = [MyEntityDAO]; ``` ## Validation and Serialization ```typescript export class CreateMyEntityVC extends ValidationClass { @IsDefined() @IsString() @MaxLength(255) @Expose() name!: string; @IsOptional() @IsString() @MaxLength(1000) @Expose() description?: string; } export class MyEntitySC extends SerializationClass { serialize(entity: MyEntity): MyEntityDTO { return { id: entity.id, name: entity.name, description: entity.description }; } } ``` ## Router ```typescript export function startMyModuleBackofficeRouter(): Router { const router = Router(); const controller = Container.get(MyModuleController); router.post('/', validateBodyParams(CreateMyEntityVC), controller.create); return router; } ``` Common middleware: `validateBodyParams(VC)`, `validateIdPathParam`, `validatePageLimitPagination({ maxLimit })`, `validateSortingParams()`, `validateStringQueryParams(VC)`. Separate router functions per API context: `startXxxAppRouter`, `startXxxBackofficeRouter`, `startXxxPublicApiRouter`. ## DI Registration ```typescript export const myModulePortsHandlers = { [MyRepositoryPort.name]: MyRepositoryAdapter, }; ``` ## Module Registration Checklist After creating a new module, register it in these locations: 1. **DAOs** in `src/api/ormModels/all.ts` — add `...myModuleDataAccessObjects` to the exported array 2. **Port handlers** in `src/api/diHandlers/serverHandlers.ts` — add `...myModulePortsHandlers` to `buildServerHandlers()` 3. **Routes** in `src/api/routes/root.ts` (APP), `backofficeRoot.ts` (backoffice), or `publicApi/root.ts` (public API) — wire the router with `router.use('/my-entities', startMyModuleBackofficeRouter())` 4. **Database migration** — run `pnpm create-migration-on-main create-my-entities-table` in `humand-packages/migrations-runner` and implement `up()` / `down()` ## Error Handling Use custom errors from `@humand-packages/common` with `ErrorCodes` enum: - `BadRequestError` (400), `NotFoundError` (404), `ForbiddenError` (403), `RequestConflictError` (409), `UnauthorizedError` (401) ## Event-Driven Architecture Controllers send queue messages after successful transactions for async processing: ``` Controller (transaction) → QueuePort.sendMessage(QUEUES.EVENT_HANDLER, QueueMessageTypes.X, payload) → EventHandlerService (SQS consumer) → handlersByMessageType[X] → port handlers ``` Queue types: `QUEUES.EVENT_HANDLER`, `QUEUES.WORKER`, etc. Message types: `QueueMessageTypes` enum in `@humand-packages/common`. ## Cross-cutting Middlewares Some concerns are wired as Express middlewares mounted in `src/api/routes/root.ts` (APP) and `backofficeRoot.ts` (BACKOFFICE), after the auth middlewares. | Middleware | Source | Purpose | |------------|--------|---------| | `buildThrottleMiddleware()` | `src/api/middlewares/throttle.ts` | Token-bucket rate limiter, Redis-backed (`CacheDomains.THROTTLE`, db 3). Per-user buckets with YAML rules at `config/throttle-rules.yaml`. Disabled by default; activate with `THROTTLE_ENABLED=true` (and optionally toggle `THROTTLE_SHADOW_MODE`). Full docs: `docs/throttle.md`. | | `validateAndSanitizeBodyParams(VC)` | `src/api/middlewares/validateAndSanitizeBodyParams.ts` | Validates the request body against `VC` AND walks the validated instance with `sanitizeWalk`. Use this in place of `validateBodyParams` on every Public API write route. The effective behavior depends on the current sanitization mode (see "Public API input sanitization" below). Walker logic: `src/api/middlewares/sanitizeWalker.ts`. Mode config: `src/api/middlewares/sanitizeModeConfig.ts`. Enforced by the architecture test at `test/architecture/publicApiSanitizationCoverage.test.ts`. | When adding a new cross-cutting middleware, prefer wiring it once in the relevant root router (after auth, before route-specific handlers) and feature-flag it via env vars so deploys are reversible without a rollback. ### Public API input sanitization Public API write endpoints run a post-validation walker (`sanitizeWalk`) that detects HTML in free-text fields without an explicit decorator. The walker is **mode-driven**, with one source of truth for the mode-per-server matrix: `humand-packages/monolith/src/api/middlewares/sanitizeModeConfig.ts` → `SANITIZE_MODE_BY_NODE_TYPE` | Mode | What the walker does | When to use | |------|----------------------|-------------| | `disabled` | No-op, no walk. | Servers not yet wired (BACKOFFICE/APP today). | | `passive` | Walks, records findings. **Does not mutate the payload.** | Observation phase before enforcement — see how often the detector would fire on real traffic. | | `permissive` | Walks, mutates HTML out of detected fields. | Once `passive` metrics look acceptable: silently scrub. | | `block` | Walks, throws `BadRequestError` with `ErrorCodes.SANITIZATION_REJECTED` listing the offending field paths. Does not mutate. | Final state for tenants/endpoints where any HTML in a plain-text field is treated as an integration bug worth surfacing. | Current matrix: each PUBLIC / APP / BACKOFFICE entry uses an inline ternary on `isProd` (resolved once at module load via `getConfig().env === Envs.PRD`). On `prd` they run in `passive` (metrics only — observation phase before enforcement); on `dev` / `stg` / `slot1` / `slot2` / `local` they run in `permissive` (mutate + emit metrics) so real integrators on lower envs and integration tests exercise the enforced behavior. BACKOFFICE / APP modes are declared but inert today — the walker only runs where `validateAndSanitizeBodyParams` is mounted, which is Public API only. All other node types: `disabled`. Flip the prd branch of those ternaries to `permissive` once Datadog signal confirms we won't mutate legitimate plain-text payloads that happen to contain angle brackets. To change the mode for a node type, edit `SANITIZE_MODE_BY_NODE_TYPE` and deploy. Do not hardcode the mode anywhere else. **Per-instance pilot override (prd PUBLIC only):** `sanitizeModeConfig.ts` carries a `PROD_PUBLIC_API_PERMISSIVE_INSTANCE_IDS` allowlist consulted by `resolveSanitizeMode(nodeType, instanceId)`. The middleware reads `instanceId` from `res.locals.loggedUser`. For an allowlisted tenant on `nodeType === PUBLIC`, the prd `passive` baseline is bumped to `permissive`. The override is scoped to PUBLIC by an explicit `nodeType` guard — if `validateAndSanitizeBodyParams` is ever mounted on APP / BACKOFFICE, the pilot does **not** silently expand there; opt those surfaces in deliberately with a separate constant. Add a tenant's id to opt in, watch `mode=permissive` for it in Datadog, then delete the constant once the prd branch of `SANITIZE_MODE_BY_NODE_TYPE[PUBLIC]` flips to `permissive` globally. Non-prd envs already run `permissive`, so the override is a no-op there. **Metrics emitted by the middleware** (`MetricRecorderPort`, Datadog backend): | Metric | Emitted when | Tags | Use | |--------|--------------|------|-----| | `humandMainApi.sanitization.requests` | **Always**, once per Public API request that runs `validateAndSanitizeBodyParams` (every mode, including `disabled`) | `nodeType`, `mode`, `vc` (top-level VC), `hasFindings: 'true' \| 'false'` | Total throughput; hit rate via `sum:requests{hasFindings:true} / sum:requests` | | `humandMainApi.sanitization.findings` | Once per finding (skipped when there are none) | `nodeType`, `mode`, `vc` (declaring VC at the finding's recursion level), `field` | Top fields/VCs by finding count | `instanceId` is **not** a metric tag — per-tenant attribution lives in logs. Each finding emits one `logger.warn('Sanitization finding', { … })` carrying `nodeType`, `mode`, `topLevelVc`, `vc`, `field`, `propertyPath` (full dotted path with indices), and `originalSnippet` (first 200 chars of the pre-strip value). `requestId` and `user.{id,instanceId}` are auto-attached by the logger via async context (set by the logger and auth middlewares earlier in the pipeline). In Datadog Logs, filter with `@user.instanceId:288690 "Sanitization finding"` for a specific tenant; combine with `@field:title`, `@vc:CreateGoalForOwnersVC`, or `@mode:permissive` to narrow further. Slice by `@nodeType` if the middleware ever gets mounted on APP / BACKOFFICE. Explicit decorators apply eagerly in **every** mode (including `disabled`). They are independent of the walker: - `@AllowHTML()` / `@Sanitize()` — HTML allowlist for fields rendered as HTML on the frontend (e.g., `Goal.description`, `KnowledgeLibrary.body`). - `@SanitizePlainTextDeep()` — recursive plain-text strip for arbitrary-JSON fields (e.g., i18n maps, profileData blobs). - `@SanitizePlainText()` — eager plain-text strip on a single field (legacy/belt-and-suspenders; new code can rely on the walker once mode flips to permissive). - `@AllowRawText()` — explicit opt-out: the walker skips this field even in `block` mode. Use for fields that legitimately accept angle brackets, ampersands, or unbalanced HTML. When writing a new Public API VC: - Use `validateAndSanitizeBodyParams(VC)` in the router (not `validateBodyParams`). - Add the appropriate explicit decorator above where it applies. Otherwise rely on the walker — the field is plain-text-only by default. - Fields with format validators (`@IsEmail`, `@IsUrl`, `@IsDateString`, `@IsEnum`, `@Matches`, etc.) are automatically skipped — the walker treats format-constrained fields as already-safe. - Custom validators (`@IsValidDate`, `@BucketUrl`, etc.) are also skipped — if a custom-validated free-text field needs sanitization, replace the custom validator or add `@AllowRawText()` and apply your own sanitizer. The architecture test at `test/architecture/publicApiSanitizationCoverage.test.ts` walks the import graph from `src/api/routes/publicApi/root.ts` and fails if any Public API router function uses `validateBodyParams` instead of `validateAndSanitizeBodyParams`. ## Testing ### Unit Tests Location: `test/` — Jest with `ts-jest`, `@faker-js/faker`. Mirrors `src/api/modules/` structure. ```bash pnpm nx run monolith:test-all pnpm nx run monolith:test-all -- --testPathPattern="test/modules/myModule" ``` Validation class test example: ```typescript import { getValidationClass } from '../../../common/serializationClasses/serializationClassesHelper'; describe('CreateMyEntityVC', () => { it('should reject empty object', () => { const { errors } = getValidationClass({}, CreateMyEntityVC); ['name'].forEach((property) => expect(errors).toPartiallyContain({ property })); }); it('should accept valid input', () => { const { errors } = getValidationClass({ name: 'test' }, CreateMyEntityVC); expect(errors).toBeEmpty(); }); }); ``` ### Integration Tests Location: `test-integration/` — testcontainers (PostgreSQL, Redis, Kafka, LocalStack). ```bash pnpm nx run monolith:test-integration-all pnpm nx run monolith:test-integration-all -- --testPathPattern="test-integration/api/myModule" ``` ``` test-integration/ ├── api/{module}/{action}.test.ts # Test files ├── commands/{module}/{action}.ts # Reusable command classes (~855) ├── sessions/sessions.ts # Session management ├── setup/ # Containers, global setup, mocks └── utils/ # Query helpers, event handlers ``` #### Sessions Sessions wrap `axios` with authentication: | Session | Auth | Usage | |---------|------|-------| | `UserSession` | None | Unauthenticated | | `LoggedSession` | `Bearer {JWT}` | Authenticated user | | `PublicAPISession` | `Basic {apiKey}` | Public API | | `BotAppLoggedSession` | `Bearer {JWT}` | Bot app | Methods: `apiPost`, `apiGet`, `apiPut`, `apiPatch`, `apiDelete`. Execute: `session.executeNoResult(command)` or `session.execute(command)`. #### Command Pattern Commands encapsulate API calls and assertions: ```typescript export class CreateKiosk extends BaseLoggedRequestCommand { constructor(input?: KiosksCreationVC) { super(input); } async _executeOn(session: LoggedSession): Promise { const res = await session.apiPost('/time-tracking/kiosk/', this.input); expect(res.status).toBe(OK); return res.data; } } ``` Base classes: `BaseLoggedRequestCommand`, `BaseUserRequestCommand`, `BasePublicRequestCommand`, `BaseBotAppsRequestCommand`. #### Community Setup Most tests start with `CreateCommunity.new()` which bootstraps an instance + admin session: ```typescript describe('My Feature', () => { let community: Community; beforeAll(async () => { community = await CreateCommunity.new(); }); it('should create entity', async () => { const result = await community.adminSession.executeNoResult( new CreateMyEntity({ name: faker.string.uuid() }), ); expect(result.name).toBeDefined(); }); }); ``` #### Test Utilities | Utility | Import | Usage | |---------|--------|-------| | `runExecuteQueryOnMainDb` | `utils/queryHelpers` | Execute raw SQL on test DB | | `runSelectQueryOnMainDb` | `utils/queryHelpers` | Select query | | `runCountQueryOnMainDb` | `utils/queryHelpers` | Count query | | `checkMessagesCountFromInsightsQueue` | `utils/insightsEventHandler` | Assert queue message count | | `getServerAdapter(PortName)` | `setup/mocks/helpers` | Access mock adapter from DI | | `getMock(PortName)` | `setup/mocks/helpers` | Get jest mock from DI | | `generateQueryToken(params)` | `utils/queryToken` | Generate signed JWT query token for endpoints behind `validateQueryToken` middleware | #### Query Token Authentication Some app router endpoints use `validateQueryToken(API_PREFIXES.APP)` middleware, which requires a signed JWT passed as a `?token=` query parameter. This token encodes `{ userId, instanceId, method, url }` and is verified server-side against the same JWT keys. If an integration test command hits a 401 `Invalid query token` error, the endpoint requires this token. Use `generateQueryToken()` from `utils/queryToken` to generate it inside the command's `_executeOn` method. Check the router file to see if `validateQueryToken` is applied to the target endpoint. #### Commands Without Input When creating a command that takes no request body, use `BaseLoggedRequestCommand` with `super()` — never use `void` as the input type. #### Writing Integration Tests Checklist 1. Create test file at `test-integration/api/{module}/{action}.test.ts` 2. Create reusable commands at `test-integration/commands/{module}/{action}.ts` if they don't exist 3. Use `CreateCommunity.new()` for setup 4. Use existing commands for prerequisites (create users, entities, etc.) 5. Assert API responses, DB state (via `queryHelpers`), and queue messages 6. Handle error cases by catching `AxiosError` and asserting `error.response.data.code` #### Test Isolation Rules (Preventing Flaky Tests) Integration tests run with `randomize: true` and `maxWorkers: 14` — multiple test files execute in parallel, in random order, sharing the same database. A test that passes in isolation can break other tests or fail depending on execution order. **Every test must be completely independent.** 1. **Never use unscoped DELETE/TRUNCATE.** `DELETE FROM "MyTable"` without a WHERE clause wipes data from all parallel tests. Always scope cleanup to data your test created: `DELETE FROM "MyTable" WHERE "instanceId" = $1` using the test's own community/instance. 2. **Never assume the database is empty.** Other test files are running concurrently and inserting data. Never assert on total counts (`SELECT COUNT(*) FROM "Table"`) — filter by your test's instanceId, userId, or other scoping column. 3. **Never use hardcoded unique values.** Use `faker.string.uuid()`, `faker.internet.email()`, or generated values for any column with a unique constraint. Two test files using the same hardcoded email will fail when they run in parallel. 4. **Every test file must be self-contained.** Create all prerequisites in `beforeAll` using `CreateCommunity.new()` and commands. Never rely on data created by another test file or on a specific execution order within the same file. 5. **Clean up only your own data.** If your test needs cleanup (e.g., to avoid unique constraint issues across retries), scope the `afterAll` cleanup to your test's own IDs/instanceId. Never clean up global tables or caches that other tests depend on. 6. **Never mutate global/shared state.** Avoid modifying `process.env`, global config, or singleton caches mid-test. If you must change a feature flag, scope it to your test's instanceId and restore it in `afterAll`. 7. **Never depend on insertion order or auto-increment IDs.** Use explicit sorting in queries or assert on specific field values, not on array positions or ID ordering. 8. **Use deterministic waits, not timing.** If testing async behavior, poll for the expected state rather than using fixed `setTimeout` delays. Delays that work locally may fail under CI load. **Before submitting tests, verify isolation:** - Could this test fail if another test file runs first and inserts conflicting data? - Could this test break other tests by deleting or modifying shared data? - Does this test pass when run alone AND as part of the full suite? ### Code Quality - All changes must pass linter: `pnpm nx run-many --target=lint` - Integration tests are mandatory for endpoint logic changes - CI uses `nx affected` to run only tests for changed modules (see `docs/adr/001-affected-test-strategy.md`). Use `scripts/validate-affected.ts` to preview what tests would run on your branch. ## Extracted packages Packages consumed by the monolith and their agent rules: - `@humand-packages/scheduled-actions` — see `humand-packages/scheduled-actions/AGENTS.md`. - `@humand-packages/community-features` — see `humand-packages/community-features/AGENTS.md`. ## Existing Modules (65) aiAgent, aiChatbot, appRatings, articles, attachments, audiences, audit, auth, billing, botApps, call, celebrations, changeDataCapture, cloudNotifications, comments, competencies, courses, crmData, deferredExecution, departments, documents, events, facialRecognition, featureFlags, files, forms, formTemplates, goals, groups, insights, instances, jobPositions, jobs, learningPaths, learningSessions, livestream, marketplace, modules, notifications, nps, oneTimeCodes, organizationCharts, peopleExperience, performanceReviews, permissions, polls, posts, prode, profileFields, questionTemplates, reactions, recognitions, referrals, regions, reports, roles, segmentations, serviceConfig, serviceManagement, signatures, timeOff, timeTracking, translations, userGatherings, users, webhooks Each module has an auto-generated `project.json` for Nx affected analysis. When adding a new module, regenerate with `scripts/generate-module-deps.ts` + `scripts/generate-module-projects.ts`.