---
url: /reference/ARCHITECTURE.md
---
# Ninaku Application Architecture

Status: **architectural baseline for `ninaku-app-api`**.

This document defines the target application architecture for Ninaku after the migration from the previous ASP.NET implementation to **NestJS**. It preserves the domain boundaries and database contracts already validated in the canonical PostgreSQL baseline while adapting the application structure to TypeScript/NestJS.

The goal is not to split Ninaku into microservices. The goal is to build **one deployable application with strong internal boundaries**.

## 1. Architectural style

Ninaku is a **modular monolith** with pragmatic DDD and Clean Architecture boundaries per module.

* One backend application.
* One deployment unit by default.
* One PostgreSQL platform database by default.
* Explicit business modules with clear ownership.
* No HTTP calls between modules inside the same process.
* No shared "god service" or generic repository spanning all domains.
* Modules collaborate through explicit in-process contracts.
* Vertical products compose reusable platform capabilities instead of duplicating them.

```mermaid
flowchart TD
    R[Restaurant] --> B[Shared business modules]
    T[Retail] --> B
    H[Hospitality] --> B
    B --> C[Platform Core]
    B --> X[Cross-cutting services]
    X --> C
    I[Optional integrations] --> R
    I --> B
    I --> X
```

The dependency direction is conceptual: verticals consume reusable capabilities; reusable capabilities consume the platform core and cross-cutting infrastructure. A vertical must not become a backdoor for mutating arbitrary data owned by another module.

## 2. Product model

Ninaku must distinguish these concepts:

* **Organization**: tenant / customer boundary.
* **Business Unit**: operating subdivision inside an organization.
* **Legal Entity**: legal/fiscal ownership boundary.
* **Brand**: commercial identity.
* **Site**: physical or logical location group.
* **Outlet**: operating point where business activity occurs.
* **Vertical**: product capability family such as restaurant, retail or hospitality.

A vertical is **not** a tenant, Business Unit, Legal Entity or Outlet.

An organization may activate different verticals for different Business Units. This is what allows a hotel group, for example, to run hospitality and food & beverage capabilities in the same Ninaku platform without creating unrelated systems.

## 3. Core principle: one business fact, one owner

Every important business fact has one owning module.

Examples:

* `sales` owns sales, sale lines, sale lifecycle and returns.
* `payments` owns payment intent, attempts, results, reversals and refunds.
* `cash` owns cash custody, shifts, counts and differences.
* `treasury` owns bank/wallet accounts, settlements, FX and reconciliation.
* `inventory` owns stock custody, lots, reservations, movements and counts.
* `accounting` owns ledgers, posting requests and journal entries.

This means:

* Sales is not Payments.
* Payments is not Cash.
* Payments is not Treasury.
* Catalog is not Inventory.
* Catalog is not Production.
* Tax is not Fiscal.
* Fiscal is not Invoicing.
* Restaurant service is not Hospitality reservations.

A user action may trigger several modules, but that does not merge their ownership.

## 4. Target source layout

The application should evolve toward the following shape:

```text
src/
├── app/
│   └── app.module.ts
│
├── bootstrap/
│   ├── configure-http-application.ts
│   ├── create-application.ts
│   └── graceful-shutdown.ts
│
├── foundation/
│   ├── config/
│   ├── correlation/
│   ├── database/
│   ├── errors/
│   ├── events/
│   ├── http/
│   ├── idempotency/
│   ├── logging/
│   ├── tenancy/
│   ├── transactions/
│   └── validation/
│
├── modules/
│   ├── reference/
│   ├── organization/
│   ├── party/
│   ├── identity/
│   ├── platform/
│   ├── assets/
│   ├── catalog/
│   ├── pricing/
│   ├── commerce/
│   ├── sales/
│   ├── settlement/
│   ├── payments/
│   ├── inventory/
│   ├── production/
│   ├── procurement/
│   ├── crm/
│   ├── loyalty/
│   ├── receivables/
│   ├── payables/
│   ├── cash/
│   ├── treasury/
│   ├── tax/
│   ├── fiscal/
│   ├── invoicing/
│   ├── accounting/
│   ├── workforce/
│   ├── documents/
│   ├── printing/
│   ├── notifications/
│   ├── communications/
│   ├── integrations/
│   ├── events/
│   ├── sync/
│   ├── audit/
│   └── analytics/
│
├── verticals/
│   ├── restaurant/
│   │   ├── service/
│   │   └── kitchen/
│   ├── retail/
│   └── hospitality/
│
└── main.ts
```

This structure is a target, not a requirement to create empty folders upfront. A folder should exist because a responsibility exists, not to satisfy a diagram.

`bootstrap/` owns the startup path: building the Nest application, applying HTTP policy to it, and orchestrating its shutdown. Grouping those three files keeps the root of `src/` readable as a list of responsibilities rather than a mix of directories and loose startup files. `main.ts` deliberately stays at the root instead of moving inside `bootstrap/`: it is the process entry point named by `package.json` (`start:prod`) and by `.railway/railway.ts`, so the file the runtime actually executes is the first one a reader sees, and the path the deployment depends on does not change.

## 5. Foundation vs business modules

`foundation/` contains technical capabilities shared by the application. It must not own restaurant, sales, hotel, inventory or accounting concepts.

Typical foundation abstractions include:

* transaction manager;
* request context;
* tenant context;
* clock;
* ID generation;
* error translation;
* event/outbox infrastructure;
* logging and tracing;
* HTTP conventions;
* validation infrastructure;
* database connection lifecycle.

Business rules live in `modules/` or `verticals/`, not in `foundation/`.

### Module scope

Foundation modules fall into two groups, and the group decides whether the module is `@Global()`.

**Ambient modules** are `@Global()`. Every module needs them, and an explicit import would only restate what the reader already assumes:

* `FoundationConfigModule` — configuration.
* `LoggingModule` — structured logging and redaction.
* `ObservabilityModule` — request and trace correlation.

**Capability modules** are imported explicitly by each consumer. A module either uses the capability or it does not, and its import list is where that answer belongs:

* `DatabaseModule` — transaction facades and database health.
* `CollectionsModule` — cursor encoding for paginated collections.
* `TelemetryModule` — HTTP metrics.

The reason is ownership, not style. Once business modules exist, "which modules touch the database?" must be answerable by reading import lists. `@Global()` erases that evidence: a module that opens transactions looks exactly like one that never reaches PostgreSQL. The cost of the explicit form is one import line per consumer, which is the point rather than the price.

This is enforced, not merely documented. `database.module.spec.ts`, `collections.module.spec.ts` and `telemetry.module.spec.ts` each compile a consumer that omits the import while the providing module is present elsewhere in the graph, and assert that Nest refuses to resolve the dependency. A capability module that regained `@Global()` would make those tests fail, because the forgotten import would silently start working again.

## 6. Module internal structure

A business module may use the following internal layout:

```text
sales/
├── domain/
├── application/
├── infrastructure/
├── presentation/
├── contracts/
└── sales.module.ts
```

Responsibilities:

### Domain

Contains business rules, entities, value objects, policies and domain events.

Domain must not depend on:

* NestJS controllers;
* HTTP;
* PostgreSQL drivers;
* an ORM;
* external SDKs.

### Application

Contains use cases, commands, queries and orchestration.

Application coordinates business behavior but does not turn itself into a generic CRUD layer.

### Infrastructure

Contains adapters for PostgreSQL, external providers, queues, storage and other technical concerns.

Infrastructure implements ports owned by the application/domain side.

### Presentation

Contains HTTP controllers, request/response DTOs and protocol translation.

Presentation must not expose persistence entities directly.

### Contracts

Contains the public in-process contracts that another module is allowed to consume.

Internal repositories, SQL builders, ORM models and implementation-specific services are not public contracts.

## 7. Module collaboration

A module writes its own business facts.

Another module must not reach into its repositories simply because the code is in the same process.

Bad:

```ts
class ConfirmSaleHandler {
  constructor(
    private readonly inventoryRepository: InventoryRepository,
    private readonly paymentRepository: PaymentRepository,
  ) {}
}
```

Preferred:

```ts
class ConfirmSaleHandler {
  constructor(
    private readonly inventory: InventoryContract,
    private readonly payments: PaymentsContract,
  ) {}
}
```

The contract may execute in the same NestJS process and the same PostgreSQL transaction, but the owning module remains responsible for its own state transitions and invariants.

There are **no internal HTTP calls** between modules of the same monolith.

## 8. API design

Ninaku APIs model business tasks, not database tables.

Preferred examples:

```text
POST /api/v1/sales/orders
POST /api/v1/sales/orders/{id}/confirm
POST /api/v1/inventory/transfers
POST /api/v1/inventory/transfers/{id}/dispatch
POST /api/v1/payments/intents
POST /api/v1/payments/{id}/capture
```

Avoid creating a public CRUD endpoint for every table.

Queries may compose read contracts from several modules when a screen needs an operational workspace. Writes remain explicit use cases.

Public response contracts should remain stable and independent from persistence implementation.

## 9. Database strategy

The canonical PostgreSQL database under `database/` is an existing platform contract and must not be casually regenerated from application models.

The application persistence layer must adapt to the current schema, constraints, RLS, functions and migration ledger.

Rules:

* PostgreSQL 18 remains the source of truth for the canonical schema.
* `ninaku_migrator` owns installation/migrations.
* `ninaku_runtime` is the application runtime role.
* RLS and tenant security remain active runtime guarantees.
* The application must not run as an owner/superuser or bypass RLS.
* Application migrations evolve the canonical database forward; an ORM must not silently rewrite or own the schema.
* The persistence technology for NestJS is a separate implementation decision and must respect these constraints.

Sharing PostgreSQL does not imply shared application ownership. Cross-schema foreign keys may exist where justified while module boundaries remain enforced at the application level.

## 10. Transactions across modules

Some operations require atomic behavior across multiple owners.

Example: confirming a sale may require Sales, Inventory and Outbox to commit atomically.

```mermaid
flowchart LR
    U[Confirm Sale] --> TX[Shared DB transaction]
    TX --> S[Sales owner writes]
    TX --> I[Inventory owner writes]
    TX --> O[Outbox write]
```

The application may share a connection, transaction and tenant context across participating module contracts.

Important rules:

* Each module still owns its writes.
* Repositories must not hide independent commits.
* A transaction boundary belongs to the use case, not to every repository call.
* External network calls are not made transactionally with PostgreSQL.
* External side effects use outbox/inbox or another recoverable pattern.

**Implemented in Foundation:** `src/foundation/transactions/transaction-runner.service.ts` is the one transaction boundary: it owns `BEGIN`/`COMMIT`/`ROLLBACK` and client release, and repositories/use cases receive a `TransactionContext` (`src/foundation/database/transaction-context.ts`) instead of opening their own transaction. `MODULE_CONTRACTS.md` §4.1 is explicit that this shared surface must not exchange `pg.PoolClient`, **raw SQL handles**, or repository implementations — so `TransactionContext` declares no members at all: a use case that receives one can do nothing with it except pass it along to a repository or another contract call. `DatabaseSession` (same file) is the queryable type a repository turns a `TransactionContext` into, in one step, via `asDatabaseSession(context)`; `PooledDatabaseSession` (`src/foundation/database/database-session.ts`) is the one concrete implementation, wrapping a checked-out `pg.PoolClient`, and it satisfies `DatabaseSession` today because that is the only shape Foundation ever constructs. `asDatabaseSession` is a plain structural type guard (checks for a callable `query` property) — no branding, `WeakMap` or registry — so a repository's own module owns the one line where opacity ends and querying begins, exactly where [MODULE\_CONTRACTS.md §8](../contracts/MODULE_CONTRACTS.md#8-cross-module-transaction-rule) says a participating module "turns that context into something that can query" for its own tables. This is a compile-time discipline, not a runtime seal: the object `asDatabaseSession` returns is the same object the whole way through, so TypeScript stops code that respects `TransactionContext`'s declared shape from calling `.query()` without going through a repository, but it cannot stop code that deliberately reaches past its own static type. Enforcing that a module's own repository only ever touches its own schema remains the import-boundary discipline [MODULE\_CONTRACTS.md §10](../contracts/MODULE_CONTRACTS.md#10-import-boundary) describes — code review today, a static check once real business modules exist. `src/foundation/tenancy/tenant-context-runner.service.ts` applies the tenant GUCs (`app.actor_identity_id`, `app.identity_id`, `app.membership_id`, `app.organization_id`, `app.impersonation_session_id`) inside that same transaction, before any domain work, using `SELECT set_config($1, $2, true)` rather than string-built `SET LOCAL app.x = '...'`. `SET LOCAL` cannot take bound parameters, so building it from a string would require interpolating identity-controlled UUID values into SQL text; `set_config(name, value, is_local)` is a normal function call and accepts the value as a query parameter while still being transaction-local (`is_local = true`, the parameterized equivalent of `SET LOCAL`), so no tenant identifier is ever concatenated into a SQL string. The impersonation GUC is always set this way, to the given value or to `NULL` when the context carries none — `set_config` with a `NULL` value resets the setting transaction-locally, the same as if it had never been set — so a value applied for one context can never survive into a later `apply` call inside the same transaction. It then calls `identity.assert_tenant_context()`; a PostgreSQL `42501` rejection is translated into a typed `TenantContextRejectedError`, never surfaced as a raw `pg` error.

**Modules use `TenantTransactionRunnerService`, never the two primitives above.** `src/foundation/transactions/tenant-transaction-runner.service.ts` composes them behind one signature, `run<T>(context: TenantContext, work: TransactionalWork<T>): Promise<T>`, so the only way to run tenant-scoped work is with the tenant context already applied and asserted before `work` starts. It delegates the transaction itself to `TransactionRunnerService.run`, wrapping `work` in a callback that first calls `TenantContextRunnerService.apply` and then invokes `work` with the same `TransactionContext`; it does not reimplement `BEGIN`/`COMMIT`/`ROLLBACK` or the release-with-rollback-error behavior, it inherits them, so a rollback that itself fails still destroys the client with the rollback error exactly as it always did. `TransactionRunnerService` and `TenantContextRunnerService` are Foundation internals: `DatabaseModule` provides `TransactionRunnerService` but does not export it, `TenancyModule` provides and exports `TenantContextRunnerService` but is not `@Global()`, so only `DatabaseModule` (which imports it) can reach it. Nest's own dependency injection refuses to compile any other module that tries to inject either one directly, so running without an applied tenant context, or applying it after domain work has already started, is not an option the API offers — this is enforced at module-boot time, not left as a convention. `src/foundation/database/database.module.spec.ts` proves both refusals. `DatabaseModule` itself is not `@Global()` either, so a module that needs `TenantTransactionRunnerService` or `SystemTransactionRunnerService` imports `DatabaseModule` and declares that dependency in its own import list; see [Module scope](#module-scope). A caller that genuinely has no tenant to apply, such as the reference-data read in `test/fixtures/collections/collections-fixture.controller.ts`, uses `SystemTransactionRunnerService` instead: an explicitly named, exported facade that opens a plain `TransactionRunnerService` transaction with no tenant context, so reaching for "no tenant" is a deliberate, visible choice rather than the path of least resistance `TransactionRunnerService` itself would otherwise be. The runner's guarantee stops at the database transaction and tenant-context lifecycle: it does not track, cancel, or clean up any other resource — a second connection, a timer, a lock, an external call — that `work` opens on its own; releasing those remains the caller's responsibility.

## 11. Idempotency and concurrency

Idempotency is required for commands where duplicate execution would produce duplicate effects, especially:

* sale confirmation;
* payment/capture;
* receiving goods;
* stock dispatch;
* posting;
* offline replay.

A retry with the same business intention must not create another operation.

Concurrency control is selected by invariant:

* optimistic version / ETag for editing drafts and configuration;
* atomic update or row locking for stock consumption/reservation;
* conditional state transitions for confirmation/posting;
* database uniqueness for one-time effects;
* leases/tokens for background workers.

Do not use an in-memory mutex as a distributed guarantee and do not default the entire system to `SERIALIZABLE` without a specific invariant requiring it.

## 12. Offline-first behavior

Offline support is not a second business implementation.

Online and offline execution converge on the same application use case.

```mermaid
flowchart LR
    ON[Online request] --> UC[Application use case]
    OFF[Offline replay] --> UC
    UC --> D[Domain rules]
    UC --> DB[(PostgreSQL)]
```

A device persists a stable command identity locally and retries the same command after reconnecting.

`sync` is responsible for transport, ordering metadata, conflict evidence and replay. It must not duplicate Sales, Inventory, Payments or other business rules.

The backend must distinguish states such as pending, confirmed, rejected and reconciliation-required where a disconnected operation cannot be globally guaranteed.

## 13. Verticals

Verticals contain capabilities that are genuinely specific to a product family.

### Restaurant

Examples:

* dining areas and tables;
* restaurant reservations;
* service sessions;
* courses;
* kitchen stations;
* kitchen work/tickets.

Restaurant consumes shared modules such as Catalog, Pricing, Sales, Payments, Inventory, Printing and Notifications.

Kitchen is not universal platform infrastructure. It is an optional food-service capability that may also be enabled inside a hotel with food & beverage operations.

### Hospitality

Hospitality should own concepts such as:

* properties/accommodation structure;
* room inventory and room types;
* hotel reservations;
* stays;
* housekeeping;
* folio-specific hospitality behavior;
* OTA/channel connectivity contracts.

Hospitality may consume Party, Pricing, Payments, Accounting, Documents and Notifications.

A hotel reservation is not the same aggregate as a restaurant table reservation.

### Retail

Retail composes the shared commerce, catalog, sales, payment and inventory capabilities without requiring restaurant/kitchen concepts.

## 14. Multi-business composition example

A hotel organization may operate multiple business models in the same platform:

```text
Organization
├── Business Unit: Hospitality
│   └── Hotel operations
└── Business Unit: Food & Beverage
    ├── Restaurant
    ├── Bar
    └── Room service
```

The same Organization can share selected platform capabilities while each Business Unit activates the vertical capabilities it actually needs.

This is preferred over adding `if (vertical === ...)` branches throughout generic core modules.

## 15. Cross-cutting modules

Capabilities such as events, sync, audit, analytics, printing, documents, notifications and integrations must remain composable.

They should not make every possible business source a mandatory dependency.

Prefer typed optional bridges/adapters when two modules need to collaborate, for example a restaurant-printing adapter connecting kitchen output with printing infrastructure.

Avoid universal polymorphic tables or services that can mutate arbitrary domain records.

## 16. Initial implementation order

Before implementing Login, Sales or Restaurant features, establish the application foundation and module boundaries.

Recommended sequence:

1. Bootstrap/application configuration.
2. PostgreSQL runtime connection using `ninaku_runtime`.
3. Request + tenant context compatible with current RLS contracts.
4. Transaction boundary abstraction.
5. Error/problem contract.
6. Idempotency foundation.
7. Event/outbox foundation.
8. Module public-contract convention.
9. Organization + Platform topology.
10. Identity/authentication/authorization.
11. Onboarding.
12. Shared operational modules such as Catalog/Sales/Inventory.
13. Vertical modules.

Do not generate all modules as empty scaffolding. Build the architectural mechanisms first, then add modules as real use cases are implemented.

## 17. Non-goals

This architecture does **not** require:

* one microservice per module;
* one database per module;
* one Nest application per vertical;
* HTTP communication between internal modules;
* an ORM-generated schema;
* a generic CRUD API;
* duplicating core capabilities inside each vertical.

Modules may be extracted into services in the future only when there is a concrete scaling, ownership or deployment reason. Extraction should be easier because the modular boundaries already exist, not because the system was distributed prematurely.

## 18. Architectural rule of thumb

When deciding where code belongs, ask:

1. Who owns this business fact?
2. Is this reusable across verticals or genuinely vertical-specific?
3. Does this module need a public contract, or am I reaching into another module's internals?
4. Does this rule belong to Domain, orchestration to Application, protocol translation to Presentation, or an adapter to Infrastructure?
5. Can this operation preserve tenant isolation, idempotency and transaction guarantees?

If ownership is unclear, do not hide the ambiguity in `shared/` or `common/`. Resolve the domain boundary first.

***

This document is the application-architecture baseline for the NestJS implementation. Historical ASP.NET architecture documents remain useful as design evidence, but new backend decisions should be documented in this repository and expressed using NestJS/TypeScript terminology.
