---
url: /reference/NESTJS_STACK_GUIDE.md
---
# Ninaku NestJS Stack Guide

Status: **technology selection guide for `ninaku-app-api`**.

This document complements the architecture, roadmap and API/schema discipline. It is informed by the NestJS official documentation and the ecosystem catalog at [Awesome NestJS](https://awesome-nestjs.com/), but it deliberately does **not** treat every listed library as a recommendation.

The goal is to keep Ninaku boring, explicit, observable and maintainable while still using strong NestJS ecosystem tooling where it genuinely helps.

The current HTTP, pagination and observability contract is owned by [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md). It expands C into C1–C6 and defines measurable acceptance for D. These are implementation requirements, not a claim that C/D or their dependencies are already implemented.

***

## 1. Dependency policy

Ninaku follows this order of preference:

1. **Node.js / PostgreSQL native capability** when it is simple and sufficient.
2. **Official NestJS package** when Nest provides a maintained integration.
3. **Small, focused third-party package** when it solves a clear gap and is actively maintained.
4. Custom implementation only when Ninaku has domain-specific requirements that generic libraries cannot represent safely.

A package is not adopted because it appears in Awesome NestJS, has many stars, or reduces a few lines of code.

Before adding a runtime dependency, answer:

* What real problem does it solve?
* Can Nest/Node/PostgreSQL already solve it adequately?
* Is the package maintained and compatible with our Nest/Node/ESM baseline?
* Does it introduce framework lock-in or hide important behavior?
* Does it preserve tenant/RLS/transaction/idempotency guarantees?
* Can it be removed later without rewriting business modules?
* What is the operational cost of running it?

Avoid "batteries included" boilerplates that impose a database model, generic CRUD architecture, generic repository pattern, microservices, GraphQL, Prisma migrations or authentication model that conflicts with Ninaku's existing contracts.

***

## 2. HTTP platform: keep Express

**Decision: keep `@nestjs/platform-express` for the current implementation.**

Reasons:

* it is Nest's default adapter;
* ecosystem compatibility is excellent;
* there is no demonstrated HTTP-adapter bottleneck today;
* the most important performance work for Ninaku is database/query design, pooling, caching where justified, payload design and horizontal scaling;
* avoiding an unnecessary adapter migration keeps Phase 0 focused.

Rules to preserve portability and clean boundaries:

* prefer normal Nest return values instead of direct `Express.Response` manipulation;
* avoid `@Res()` unless streaming/file/protocol behavior truly requires native response access;
* avoid business logic in Express middleware;
* Domain/Application code must never import Express types;
* Express-specific integrations stay at the Presentation/Foundation edge.

If future measurements show Express itself is a material bottleneck, adapter replacement can be evaluated with representative k6 scenarios instead of synthetic `/health` benchmarks.

***

## 3. Configuration: official `@nestjs/config` + Zod

**Target: adopt early (Phase 0).**

Packages:

```text
@nestjs/config
zod                 # already present
```

NestJS 12 supports Standard Schema validation and recommends modern schema libraries such as Zod for new projects.

Use one typed configuration boundary and fail fast during startup.

Preferred direction:

```ts
ConfigModule.forRoot({
  validationSchema: z.object({
    NODE_ENV: z.enum(['local', 'test', 'development', 'staging', 'production']),
    PORT: z.coerce.number().int().positive().default(3000),
    DATABASE_URL: z.string().min(1),
  }),
});
```

Rules:

* do not read `process.env` throughout business modules;
* configuration is grouped by technical concern;
* secrets never receive default production values;
* optional integration credentials are required only when that integration is enabled;
* tests construct explicit configuration rather than depending on a developer machine.

Do **not** add another config library (`nest-typed-config`, `configfy`, etc.) unless the official module demonstrably cannot satisfy a requirement.

**Implemented in Foundation:** `NODE_ENV`, `PORT`, and discrete `DATABASE_HOST`/`DATABASE_PORT`/`DATABASE_NAME`/`DATABASE_USER`/`DATABASE_PASSWORD`/`DATABASE_SSL` variables instead of a single `DATABASE_URL`, because generated base64 passwords contain `/`, `+`, `=` characters that break a connection URL. `ConfigModule.forRoot({ validate })` uses a Zod schema and fails fast naming the invalid keys, never their values.

***

## 4. Request validation: Nest 12 Standard Schema + Zod

**Target: Foundation C3 (Phase 2).**

NestJS 12 includes `StandardSchemaValidationPipe`, so Ninaku can use Zod without requiring a third-party Nest/Zod bridge for basic validation.

Preferred model:

```text
HTTP input
   ↓
Zod schema / DTO contract
   ↓
StandardSchemaValidationPipe
   ↓
Application command/query
```

Rules:

* external input is validated at the boundary;
* domain invariants are still enforced in Domain/Application and are not replaced by DTO validation;
* parsing/coercion must be intentional;
* IDs, money, dates, quantities and enums use explicit contracts;
* validation schemas are not database entities.

C3 includes body, path, query and relevant headers, unknown-field policy, size/range limits and missing-versus-null semantics. The validation failure is the common C4 `errors` list; see the normative Foundation specification.

`nestjs-zod` appears in Awesome NestJS and may be useful if its OpenAPI/DTO ergonomics materially improve our implementation, but it is **not a default dependency** because Nest 12 already supports Standard Schema natively.

***

## 5. OpenAPI: official `@nestjs/swagger`

**Target: Foundation C6, before business modules.**

Package:

```text
@nestjs/swagger
```

OpenAPI is part of the public API contract, not decoration added at the end.

Use it to document:

* route purpose;
* input/output DTOs;
* pagination/cursors;
* authentication requirements;
* Problem Details responses;
* idempotency/precondition headers where applicable;
* versioned business operations.

Rules:

* do not generate CRUD endpoints from entities merely to obtain Swagger;
* C6 generates/validates OpenAPI in CI and detects incompatible changes;
* HTTP tests prove the implemented response matches the contract and protocol exceptions;
* the API contract must remain independent from persistence entities;
* generated SDKs/mocks can be evaluated later from the OpenAPI artifact.

C1 defines natural requests, `data` with optional `meta`, headers and status codes. C2 defines `meta.pageInfo`, bounded lists and opaque/versioned/protected cursors. The owner of these Ninaku conventions is [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md).

Awesome NestJS lists automatic CRUD/OpenAPI frameworks. These conflict with Ninaku's task-oriented API rule and should not define our public API.

***

## 6. Error model: Ninaku Problem Details

**Target: Foundation C4.**

Ninaku already defines RFC 9457-style `application/problem+json` responses with stable extensions such as:

```text
code
traceId
retryable
errors
```

`errors`, when present, is always a list using the shared location/field/code/message shape. Errors are not wrapped in `data`; the body's status agrees with HTTP. `retryable` alone never authorizes repeating a non-idempotent command. The current examples and acceptance tests live in the normative Foundation specification.

Awesome NestJS lists `nest-problem-details`, which is a useful reference and a candidate for evaluation, but Ninaku has domain-specific requirements around:

* stable error codes;
* safe PostgreSQL constraint translation;
* tenant-safe 404 behavior;
* idempotency conflicts;
* retryability;
* field validation errors;
* trace correlation.

Therefore the default direction is a **small Ninaku-owned exception/error mapping layer** built on Nest exception filters and `HttpAdapterHost`, unless the third-party library proves it can express our contract cleanly without leaking its model into Domain/Application.

Domain/Application code returns typed failures and does not construct HTTP responses.

***

## 7. PostgreSQL access: `pg` first, typed query builder evaluated separately

**Target: foundational decision before business modules.**

The canonical PostgreSQL schema is authoritative. Application tooling must adapt to it, not regenerate it.

Baseline runtime candidate:

```text
pg (node-postgres)
```

Why:

* mature PostgreSQL driver;
* explicit pool/client lifecycle;
* complete SQL access;
* works naturally with PostgreSQL functions, RLS and advanced SQL;
* transactions are explicit and require the same checked-out client, which matches Ninaku's transaction-boundary design.

A higher-level typed query builder may be layered over `pg`, but it must not own migrations/schema evolution.

### Kysely

**Strong candidate to evaluate.**

Useful because it is a type-safe SQL query builder rather than a schema-owning ORM. It can use the `pg` pool and keeps SQL concepts visible.

Potential fit:

```text
canonical PostgreSQL
       ↓ introspection/codegen
TypeScript DB types
       ↓
Kysely query builder
       ↓
module persistence adapters
```

### Drizzle

**Candidate, but with stricter guardrails.**

Drizzle supports introspecting an existing PostgreSQL database and using `pg`, but its migration/push tooling must not become an alternative schema authority.

For Ninaku, commands such as schema push/generate/migrate would be disabled from the normal application workflow unless explicitly approved as part of the canonical migration process.

### Prisma / TypeORM

Not automatically rejected, but they are currently lower-priority candidates because Ninaku has:

* a large existing PostgreSQL schema;
* native functions/triggers/RLS;
* explicit SQL migration contracts;
* no desire for an ORM to regenerate the database model.

The final persistence choice must be proven with a real slice (`organization` or `identity`) including transactions, RLS context, functions and tests before becoming the platform standard.

**Implemented in Foundation:** `src/foundation/database` builds a `pg.Pool` from typed config, applying `DATABASE_POOL_MAX` as `max`, `DATABASE_CONNECTION_TIMEOUT_MS` as `connectionTimeoutMillis`, and `DATABASE_STATEMENT_TIMEOUT_MS`/`DATABASE_LOCK_TIMEOUT_MS`/`DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_MS` as the matching `pg` session options (`statement_timeout`, `lock_timeout`, `idle_in_transaction_session_timeout`). The pool closes via its `OnModuleDestroy` hook, which `app.close()` always triggers; `src/bootstrap/graceful-shutdown.ts` calls `app.close()` on SIGTERM/SIGINT before draining telemetry, so no separate `enableShutdownHooks()` call is needed. `registerGracefulShutdown` returns that same memoised shutdown to `src/main.ts`, which invokes it when startup fails after the application was already built — `app.listen()` failing to bind the port is the realistic case. Without it the process would exit while a fully constructed application, and the pool it had already opened, were never closed. Returning the shutdown the signal listeners themselves use, rather than building a second one, is what makes this safe: a startup failure that races a SIGTERM still closes the pool exactly once, and the telemetry drain still runs even when `app.close()` throws, because the orchestrator drains it in a `finally`. Boot fails if the connected role is superuser or `BYPASSRLS`.

***

## 8. Request / tenant / transaction context

Ninaku needs per-request context for values such as:

```text
traceId
principalId
organizationId
businessUnitId
legalEntityId
siteId
outletId
transaction client/context
```

Do not solve this by converting the whole dependency graph to Nest `REQUEST` scope.

Preferred options to evaluate:

1. Node.js `AsyncLocalStorage` directly behind a Ninaku abstraction.
2. `nestjs-cls` as a maintained Nest-friendly ALS wrapper.

`nestjs-cls` is listed by Awesome NestJS and is actively maintained. It can be useful, but Ninaku must still explicitly test context propagation across:

* HTTP requests;
* nested async calls;
* database transactions;
* queue workers;
* scheduled jobs;
* tests.

ALS/CLS is a transport mechanism for context, **not authorization**. Every protected operation still validates permissions and PostgreSQL RLS remains a security boundary.

***

## 9. Logging: Nest JSON behind a small Ninaku boundary

**Status: implemented in Foundation C5** (`src/foundation/logging`; see [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md) section 7.8). Nest's `LoggerService` contract is implemented directly by `NinakuLoggerService` instead of wrapping `ConsoleLogger`'s `json` mode, because that mode does not produce the ISO timestamp or five-level severity the record shape requires; the same substitution point (`app.useLogger()`) stays available for `nestjs-pino` if that alternative is adopted later. Do not build a logger from scratch or expose a telemetry storage vendor to business modules.

The exact record and acceptance matrix live in [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md). Common fields are timestamp, level, event, service, serviceVersion and environment. Request, HTTP, module, operation, error and permitted organization fields are conditional on valid context.

Technical JSON uses `trace_id`/`span_id` for actual W3C/OpenTelemetry context; the public Problem Details extension remains `traceId`. Keep requestId, traceId and stable business commandId separate. Do not invent spans for startup logs or treat telemetry headers as trusted tenant identity.

Required policy:

* one JSON record per line, bounded in size and structure;
* allowlisted fields instead of complete requests, responses, users, headers or SQL parameters;
* secret protection in error messages/causes, URLs, provider responses and captured/exported telemetry as well as ordinary records;
* no passwords, PIN/OTP, access/refresh tokens, cookies, API secrets, connection strings, payment credentials, private certificate material or full sensitive payloads;
* one responsible layer records an unexpected failure; no repeated stack at every layer;
* expected validation/business rejection does not automatically generate urgent error alerts;
* successful health checks are suppressed or sampled and production debug is off by default;
* durable business/security audit evidence remains separate and is not sampled;
* define retention, access, volume budget, bounded queues, export timeouts and shutdown/loss behavior;
* prove staging lookup by response identifier and correlation to instrumented execution, not only console output.

`nestjs-pino` remains an alternative when compatible, tested and measurably useful. The same contract and security/performance tests apply; replacing the adapter must not rewrite business modules.

***

## 10. Health/readiness: official `@nestjs/terminus`

**Target: adopt in Phase 0.**

Package:

```text
@nestjs/terminus
```

Expose separate concepts:

```text
/health/live
/health/ready
```

`live` answers whether the process/event loop is alive enough to serve.

`ready` answers whether this instance should receive production traffic.

Readiness may include a lightweight PostgreSQL check and required runtime dependencies, but must not call every external provider on every probe.

Enable Nest shutdown hooks and support graceful shutdown/connection draining.

***

## 11. Rate limiting: Cloudflare first, `@nestjs/throttler` for application semantics

**Target: adopt selectively.**

Awesome NestJS lists the official `@nestjs/throttler` module.

Use two layers for different problems:

```text
Cloudflare
  └─ coarse abuse / DDoS / edge rate limiting

Nest @nestjs/throttler
  └─ application-aware limits (login, OTP, sensitive operations)
```

Do not depend only on an in-memory throttler store once multiple replicas exist. Distributed limits require a shared store or edge enforcement.

Rate limits must account for trusted proxy headers correctly when running behind Cloudflare/Railway.

***

## 12. Caching: official `@nestjs/cache-manager`, but only by use case

**Status: optional, not Phase 0 mandatory.**

Package candidate:

```text
@nestjs/cache-manager
```

Good candidates:

* global/reference data;
* public catalog projections;
* expensive but reconstructable read models;
* short-lived provider metadata.

Bad candidates:

* current stock as source of truth;
* payment state;
* sale state;
* accounting balances;
* authorization truth without a robust invalidation contract.

Do not enable global response caching blindly on authenticated tenant APIs.

Redis/Valkey becomes a storage option only when multi-instance/shared cache requirements exist.

***

## 13. Redis: optional infrastructure, not a database replacement

Awesome NestJS lists multiple Redis integrations. We should not choose one until a real use case exists.

Potential uses:

* shared cache;
* distributed rate-limit state;
* BullMQ;
* ephemeral presence/realtime state;
* explicitly designed distributed coordination.

Redis must not become the authoritative record for Sales, Inventory, Payments, Fiscal or Accounting.

If Redis is introduced, select one client/integration standard and centralize it in Foundation rather than allowing modules to instantiate clients independently.

***

## 14. Background jobs: official `@nestjs/bullmq` when durable async work is required

**Status: adopt when the first durable asynchronous workflow exists.**

Packages:

```text
@nestjs/bullmq
bullmq
```

Good candidates:

* fiscal/SRI delivery and reconciliation;
* email/WhatsApp delivery;
* provider webhooks;
* document generation;
* OTA synchronization;
* analytics projection work;
* retryable integration tasks.

Critical rule:

```text
business transaction
    ├─ domain writes
    └─ outbox write
          ↓ commit
outbox dispatcher
          ↓
BullMQ
          ↓
idempotent worker
          ↓
external provider
```

Do not `COMMIT` business state and then simply `queue.add()` hoping the process does not crash between the two operations.

Workers must be idempotent and observable.

***

## 15. In-process events: `@nestjs/event-emitter` only for non-durable notifications

**Status: optional.**

The official Nest event-emitter module is useful to decouple in-process reactions.

Use it only when losing the event on process crash is acceptable or when the durable fact is independently recoverable.

Do **not** use an in-memory event emitter as a replacement for:

* outbox;
* integration events;
* financial/fiscal workflows;
* reliable notifications;
* cross-instance coordination.

Domain events may be dispatched in process after/around transaction orchestration, but durable external consequences require persisted evidence.

***

## 16. Scheduling: official `@nestjs/schedule` with replica-safe rules

**Status: optional.**

`@nestjs/schedule` is fine for simple local scheduling, but multiple application replicas can execute the same cron simultaneously.

Therefore a scheduled task must satisfy at least one of these:

* duplicate execution is harmless and idempotent;
* database claim/lease ensures only one worker performs the job;
* it runs in a dedicated worker/scheduler instance;
* the platform scheduler invokes a normal idempotent endpoint/job.

Do not use a process-local cron as the only guarantee for critical fiscal, payment or settlement work.

***

## 17. CQRS: do not adopt `@nestjs/cqrs` by default

Awesome NestJS lists the official `@nestjs/cqrs` and typed wrappers around it.

Ninaku already distinguishes commands, queries and domain ownership conceptually. We do **not** need a framework package merely to name classes `Command` and `Query`.

Start with simple application handlers/services and explicit module contracts.

Evaluate `@nestjs/cqrs` only when we have concrete value from:

* standardized command/query buses across many modules;
* decorators/pipelines around handlers;
* sagas/process managers;
* enough complexity that the bus reduces rather than increases indirection.

Avoid architecture ceremony without a use case.

***

## 18. Authorization: own Ninaku authorization model

Awesome NestJS lists generic RBAC libraries and external policy engines.

Ninaku already has a richer existing model:

```text
principal
membership
role / permission / capability
Organization
Business Unit
Legal Entity
Brand
Site
Outlet
RLS
```

Generic `role === admin` packages must not replace this model.

Nest Guards/Decorators can provide the HTTP integration, but effective authorization is resolved through Ninaku Identity/Platform contracts and validated against the same scope model protected by PostgreSQL.

External engines such as Keycloak/OSO/Permit may be evaluated for specific future requirements, not adopted before proving they fit our scope/history/multi-business model.

***

## 19. Multi-tenancy: do not outsource the tenant model to a generic package

Awesome NestJS lists generic multitenancy packages such as `nestjs-mtenant`.

Ninaku's tenancy is already encoded in the domain and database contracts. It is not merely "choose database/schema by request".

Our runtime needs to propagate authorized context into PostgreSQL RLS and module contracts.

A generic multitenancy module may inspire implementation techniques, but it must not become the source of tenant ownership or bypass the existing Organization/Business Unit/Legal Entity model.

***

## 20. Testing: Vitest remains the standard

Current repository standard:

```text
Vitest
Supertest
native PostgreSQL contract/integration tests
```

Keep this baseline.

Testing layers:

```text
Domain unit tests
Application unit tests
Persistence integration tests
Module integration tests
HTTP e2e tests
Database native contract/concurrency tests
Load/resilience tests (k6)
```

`@golevelup/ts-vitest` is listed by Awesome NestJS and can be evaluated when it materially simplifies typed mocks. Do not add mocking libraries merely to make every dependency a mock; integration tests against real PostgreSQL are essential for RLS, constraints, functions, locking and transaction behavior.

OpenAPI-driven mocks/Pact-style testing may become useful for external consumers/providers later. C6's own OpenAPI generation, schema validation, runtime-contract tests and incompatible-change detection are required in Foundation, not deferred to that future evaluation.

***

## 21. Observability: OpenTelemetry and W3C Trace Context

**Decision for Foundation C5:** OpenTelemetry with W3C Trace Context and configurable export. The instrumentation strategy is selected; compatible packages, storage destination and operational limits still require implementation evidence. Approving this document does not provision a provider.

**Status: bootstrap implemented in Foundation C5** (`src/foundation/telemetry`; see [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md) section 7.8) with `@opentelemetry/instrumentation-http`/`-pg`, a redacting exporter wrapper, and bounded batching/shutdown. The exporter stays disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured; the storage destination, sampling policy and retention/access/cost budget remain pending owner confirmation.

Keep technical logs, traces, metrics and durable audit separate. Do not run overlapping APM SDKs or put runtime HTTP logs into the transactional database.

The previously evaluated `@nestjs/observe` path is not a second mandatory SDK. An alternative must preserve the agreed contract and pass compatibility, security, exportability and performance review before replacing the selected strategy.

C5 must demonstrate:

* useful HTTP/database spans, not only a traceId field;
* request/log/trace correlation without trusting trace headers for authorization;
* data minimization and protection at capture and export;
* bounded attribute cardinality, queues, retries, timeouts and shutdown;
* documented sampling, access, retention and volume/cost policy;
* staging lookup of a response identifier and its instrumented execution;
* exporter-failure behavior without unbounded API memory growth.

The exact policy and acceptance tests are in [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md). Extend the same conventions to jobs/integrations as those paths are implemented. A Collector is an optional deployment choice, not automatically required additional infrastructure.

***

## 22. Security/HTTP hardening

Use Nest/Express ecosystem primitives deliberately:

* Helmet/security headers where appropriate;
* strict CORS allowlists per environment;
* body-size limits;
* secure cookie configuration if cookies are used;
* trusted-proxy configuration for Cloudflare/Railway;
* request timeouts and upstream timeouts;
* endpoint-specific rate limiting;
* redacted logs;
* validation before application handling.

Security-critical behavior must be explicit in bootstrap/Foundation and covered by e2e tests.

***

## 23. Recommended adoption matrix

| Capability | Preferred choice | Status |
|---|---|---|
| HTTP adapter | `@nestjs/platform-express` | **Keep** |
| Config | `@nestjs/config` + Zod | **Adopt Phase 0** |
| Input validation | Nest `StandardSchemaValidationPipe` + Zod | **Foundation C3** |
| OpenAPI | `@nestjs/swagger` | **Foundation C6 with CI verification** |
| Health/readiness | `@nestjs/terminus` | **Adopt Phase 0** |
| PostgreSQL driver | `pg` | **Strong baseline candidate** |
| Typed SQL | Kysely | **Evaluate** |
| ORM alternative | Drizzle | **Evaluate with strict schema guardrails** |
| Request context | native AsyncLocalStorage | **Foundation B** |
| Logging | Nest JSON behind a small Ninaku boundary | **Foundation C5** |
| Pino | `nestjs-pino` | **Alternative if justified; same contract/tests** |
| Rate limiting | Cloudflare + `@nestjs/throttler` | **Adopt selectively** |
| Cache | `@nestjs/cache-manager` | **Use case driven** |
| Redis | chosen client/integration later | **Not mandatory** |
| Durable jobs | `@nestjs/bullmq` + BullMQ | **Adopt when required** |
| Local events | `@nestjs/event-emitter` | **Optional / non-durable only** |
| Scheduling | `@nestjs/schedule` | **Optional / replica-safe only** |
| CQRS package | `@nestjs/cqrs` | **Do not adopt yet** |
| Generic RBAC | third-party RBAC package | **Do not adopt by default** |
| Generic multitenancy | third-party tenant package | **Do not adopt as domain model** |
| Unit/e2e testing | Vitest + Supertest | **Keep** |
| Test mock helpers | `@golevelup/ts-vitest` | **Optional** |
| Tracing/APM | OpenTelemetry + W3C Trace Context | **Foundation C5; implementation/backend to verify** |

***

## 24. Packages/styles we intentionally avoid by default

Do not introduce these patterns without an explicit architectural review:

* generic CRUD generators/controllers;
* generic repository abstractions spanning unrelated domains;
* schema-first ORM migration ownership over canonical SQL;
* automatic GraphQL layer just because a boilerplate includes it;
* microservice transports inside the monolith;
* event sourcing for ordinary CRUD/business state;
* CQRS buses everywhere without a use case;
* Redis-backed locks as the first answer to PostgreSQL concurrency problems;
* request-scoped providers throughout the application;
* global cache on authenticated operational endpoints;
* process-local cron as a uniqueness guarantee;
* role-only authorization that ignores business scope;
* boilerplates that replace Ninaku's module/domain boundaries;
* ternary expressions in TypeScript source, enforced by `npm run lint:no-ternary`.

***

## 25. Phase 0 technology spike

Before the first business module, prove a thin vertical technical slice:

```text
Express HTTP
   ↓
typed config
   ↓
request/trace context
   ↓
Zod input validation
   ↓
PostgreSQL runtime pool (`ninaku_runtime`)
   ↓
transaction + SET LOCAL tenant/RLS context
   ↓
health/readiness
   ↓
structured logs/traces
   ↓
OpenAPI
```

The spike should include:

* one public health endpoint;
* one protected test endpoint;
* one RLS-protected PostgreSQL read;
* one transactional PostgreSQL write in a disposable test database;
* a denied cross-tenant access test;
* graceful shutdown test/manual verification;
* configuration failure test;
* trace/request ID visible in logs/errors;
* basic k6 smoke baseline.

Once this works cleanly, the same foundation is reused by real modules instead of discovering infrastructure rules while building Sales/Auth.

Issue #13 now tracks four main slices A–D, with C delivered as C1–C6 rather than exactly four PRs overall. [FOUNDATION\_HTTP\_AND\_OBSERVABILITY.md](../contracts/FOUNDATION_HTTP_AND_OBSERVABILITY.md) is the owner of the expanded scope and acceptance matrix. Required additions to the spike are:

* success/error/collection contract tests, including protocol exceptions;
* cursor integrity/scope/authorization tests and explicit consistency;
* secret-leak tests against actual serialized/exported output;
* concurrent request/tenant/trace isolation;
* staging log/trace lookup using the response identifier;
* bounded telemetry export failure and shutdown behavior;
* OpenAPI generation/validation, runtime agreement and incompatible-change checks;
* a reproducible D report measuring available paths and instrumentation cost, without extrapolating health-check results to business capacity.

Decisions on cursor protection/expiry and telemetry backend, access, retention and budgets must be resolved in C2/C5. Describing these outcomes does not mark implementation complete or enable production; go-live remains issue #7.

***

## 26. Guiding principle

Use the NestJS ecosystem to **remove accidental complexity**, not to outsource Ninaku's architecture.

A good dependency should make one technical concern easier while leaving Domain/Application ownership more explicit.

If adding a package makes it harder to answer "who owns this rule, transaction or business fact?", it is probably the wrong abstraction for Ninaku.
