Skip to content

API Module Architecture

Module Dependency Graph

The API is a modular monolith. AppModule is a pure orchestrator that imports four module categories: config/infra (SharedModule), cross-cutting (CoreModule), and feature modules (IdentityModule, NotificationsModule, PlansModule, CatalogModule).

graph TD
  App["<b>AppModule</b><br/><i>Orchestrator</i>"]

  Shared["<b>SharedModule</b><br/><i>@Global</i>"]
  Config["<b>AppConfigModule</b><br/><i>@Global</i>"]
  Core["<b>CoreModule</b><br/><i>@Global</i>"]
  Identity["<b>IdentityModule</b><br/><i>Feature</i>"]
  Notifications["<b>NotificationsModule</b><br/><i>Feature</i>"]
  Plans["<b>PlansModule</b><br/><i>Feature</i>"]
  Catalog["<b>CatalogModule</b><br/><i>Feature</i>"]
  Router["<b>RouterModule</b><br/><i>/identity, /notifications, /plans, /catalog</i>"]

  App --> Shared
  App --> Core
  App --> Router
  App --> Identity
  App --> Notifications
  App --> Plans
  App --> Catalog

  Shared --> Config

  Identity -.->|"HashingServiceKey<br/>EncryptionServiceKey"| Shared
  Identity -.->|"APP_GUARD<br/>(JwtAuthGuard, AuthTypeGuard)"| Core
  Notifications -.->|"AppConfigService"| Shared
  Notifications -.->|"APP_GUARD"| Core
  Plans -.->|"PostgresMetricsServiceKey"| Shared
  Plans -.->|"APP_GUARD"| Core
  Catalog -.->|"PostgresMetricsServiceKey"| Shared
  Catalog -.->|"APP_GUARD"| Core

  Identity -. "UserRegisteredEvent<br/>ClientRegisteredEvent<br/>(EventBus)" .-> Notifications

  style App fill:#4a90d9,color:#fff,stroke:#2a5f9e
  style Shared fill:#7b68ee,color:#fff,stroke:#5a4cbf
  style Config fill:#9370db,color:#fff,stroke:#6a4fb5
  style Core fill:#e67e22,color:#fff,stroke:#c06a1a
  style Identity fill:#27ae60,color:#fff,stroke:#1e8449
  style Notifications fill:#27ae60,color:#fff,stroke:#1e8449
  style Plans fill:#27ae60,color:#fff,stroke:#1e8449
  style Catalog fill:#27ae60,color:#fff,stroke:#1e8449
  style Router fill:#95a5a6,color:#fff,stroke:#7f8c8d

Module Responsibilities

ModuleScope@GlobalKey Exports
AppModuleRoot orchestratorNo
SharedModuleCross-cutting infra servicesYesHashingServiceKey, EncryptionServiceKey
AppConfigModuleEnv validation + typed configYesAppConfigService
CoreModuleGlobal HTTP guards + request observability interceptorYesAPP_GUARD, APP_INTERCEPTOR
IdentityModuleAuth, users, clients, agenciesNoIdentityRepositoryProviders, IdentityInfrastructureAdapters (incl. IOrganizationFlagsGateway)
NotificationsModuleEmail delivery + loggingNo
PlansModulePublic plans catalog + internal plan CRUDNoPlansRepositoryProviders, PlansAdapterProviders (incl. ISubscriptionLimitsGateway)
CatalogModuleService configuration catalog CRUDNoIServiceConfigurationRepositoryKey

Dormant AI Module Code Path

The repository also contains an AIModule, but it is not part of the mounted dependency graph yet.

  • AIModule already creates the dedicated ai TypeORM connection and registers AI persistence entities.
  • The module defines controller and provider registries in source, but ai.module.ts still leaves controllers and providers empty.
  • AppModule does not import or route AIModule, so the AI HTTP surface is not currently live.
  • See the dedicated AI docs for the implemented source-level behavior and activation blockers: /modules/ai/

Global Modules Detail

SharedModule

Provides cross-cutting infrastructure services. Imported once by AppModule; available everywhere because of @Global().

graph LR
  subgraph SharedModule["SharedModule (@Global)"]
    direction TB
    AC["AppConfigModule"]
    ES["EncryptionService"]
    HS["HashingService"]
    ESK["EncryptionServiceKey<br/><i>useExisting → EncryptionService</i>"]
    HSK["HashingServiceKey<br/><i>useExisting → HashingService</i>"]

    ES --> ESK
    HS --> HSK
  end

  AC -->|"AppConfigService"| ES

  style SharedModule fill:#f5f0ff,stroke:#7b68ee
  style ESK fill:#e8e0ff,stroke:#7b68ee
  style HSK fill:#e8e0ff,stroke:#7b68ee

CoreModule

Registers global HTTP guards and the request observability interceptor.

  • Guards run on every request unless bypassed by @PublicRoute().
  • RequestObservabilityInterceptor wraps each HTTP request with:
    • canonical request log (http.request.completed)
    • server span (SpanKind.SERVER) with request metadata
    • request ID propagation (x-request-id)
graph LR
  subgraph CoreModule["CoreModule (@Global)"]
    direction TB
    JWT["JwtAuthGuard<br/><i>extends AuthGuard('jwt-access')</i>"]
    ATG["AuthTypeGuard<br/><i>checks @AuthTypes() metadata</i>"]
  end

  subgraph Decorators["core/decorators/"]
    PR["@PublicRoute()"]
    AT["@AuthTypes('user' | 'client')"]
    Auth["@Auth()"]
    CA["@ClientAuth()"]
  end

  PR -->|"bypasses"| JWT
  AT -->|"checked by"| ATG
  Auth -->|"sets AuthTypes('user')"| AT
  CA -->|"sets AuthTypes('client')"| AT

  style CoreModule fill:#fff5eb,stroke:#e67e22
  style Decorators fill:#fef9f3,stroke:#e67e22

Feature Modules

IdentityModule — DDD Layers

Each feature module follows DDD layering: Domain (entities, repository interfaces, events) → Application (commands, service ports) → Infrastructure (TypeORM, Passport, external services) → Presentation (controllers).

graph TB
  subgraph Presentation["Presentation Layer"]
    UAC["UserAuthController<br/>/auth/user"]
    CAC["ClientAuthController<br/>/auth/client"]
    SAC["AuthController<br/>/auth"]
  end

  subgraph Application["Application Layer"]
    direction TB
    UCH["User Commands<br/><i>Register, Login, GoogleLogin,<br/>VerifyEmail, SwitchAgency</i>"]
    CCH["Client Commands<br/><i>Register, Login, GoogleLogin,<br/>VerifyEmail</i>"]
    SCH["Shared Commands<br/><i>RefreshToken, Logout</i>"]
    TSP["ITokenService"]
    GCAS["IGoogleClientAuthService"]
  end

  subgraph Domain["Domain Layer"]
    direction TB
    UE["User"]
    AE["Agency"]
    CE["Client"]
    RT["RefreshToken"]
    UVT["UserVerificationToken"]
    CVT["ClientVerificationToken"]
    UAE["UserAgency"]
    EV["Events:<br/>UserRegisteredEvent<br/>ClientRegisteredEvent"]
    RI["Repository Interfaces<br/><i>7 Symbol-based ports</i>"]
  end

  subgraph Infrastructure["Infrastructure Layer"]
    direction TB
    subgraph Auth["Auth"]
      TS["TokenService"]
      GCS["GoogleClientAuthService"]
      STR["Passport Strategies<br/><i>jwt-access, jwt-refresh,<br/>local-user, local-client,<br/>google-user</i>"]
    end
    subgraph Persistence["Persistence"]
      PE["TypeORM Entities<br/><i>7 persistence classes</i>"]
      REPO["Repository Impls<br/><i>7 implementations</i>"]
      MAP["Mappers<br/><i>domain ↔ persistence</i>"]
    end
    DB[("PostgreSQL<br/>identity schema")]
  end

  UAC --> UCH
  UAC --> SCH
  CAC --> CCH
  SAC --> SCH

  UCH --> RI
  CCH --> RI
  SCH --> RI
  UCH --> TSP
  CCH --> TSP
  SCH --> TSP
  CCH --> GCAS

  TS -.->|implements| TSP
  GCS -.->|implements| GCAS
  REPO -.->|implements| RI

  REPO --> PE
  REPO --> MAP
  PE --> DB

  UCH -->|publishes| EV
  CCH -->|publishes| EV

  style Presentation fill:#e8f5e9,stroke:#27ae60
  style Application fill:#e3f2fd,stroke:#1976d2
  style Domain fill:#fff3e0,stroke:#f57c00
  style Infrastructure fill:#fce4ec,stroke:#c62828
  style Auth fill:#fce4ec,stroke:#c62828
  style Persistence fill:#fce4ec,stroke:#c62828

NotificationsModule — DDD Layers

graph TB
  subgraph Application["Application Layer"]
    direction TB
    SENH["SendEmailNotificationHandler"]
    URNH["UserRegisteredNotificationHandler"]
    CRNH["ClientRegisteredNotificationHandler"]
    ESP["IEmailService"]
    TRP["ITemplateRegistryService"]
  end

  subgraph Domain["Domain Layer"]
    NL["NotificationLog"]
    NLR["INotificationLogRepository"]
    NT["NotificationType"]
  end

  subgraph Infrastructure["Infrastructure Layer"]
    direction TB
    NES["NodemailerEmailService"]
    TRS["TemplateRegistryService"]
    TPL["React Email Templates<br/><i>UserRegistrationVerification<br/>ClientRegistrationVerification</i>"]
    NLRI["NotificationLogRepositoryImpl"]
    NLP["NotificationLogPersistence"]
    DB[("PostgreSQL<br/>notifications schema")]
  end

  URNH -->|"executes"| SENH
  CRNH -->|"executes"| SENH
  SENH --> ESP
  SENH --> TRP
  SENH --> NLR

  NES -.->|implements| ESP
  TRS -.->|implements| TRP
  NLRI -.->|implements| NLR

  TRS --> TPL
  NLRI --> NLP
  NLP --> DB

  style Application fill:#e3f2fd,stroke:#1976d2
  style Domain fill:#fff3e0,stroke:#f57c00
  style Infrastructure fill:#fce4ec,stroke:#c62828

Notifications Email Subject Policy

  • NodemailerEmailAdapter prefixes subjects only outside production:
    • development -> [DEVELOPMENT] <subject>
    • test -> [TEST] <subject>
    • production -> no prefix

Data Flow Diagrams

Authentication Flow (User Login)

sequenceDiagram
  participant C as Client
  participant JG as JwtAuthGuard
  participant LUS as LocalUserStrategy
  participant UAC as UserAuthController
  participant LUH as LoginUserHandler
  participant TS as TokenService

  C->>+UAC: POST /identity/auth/user/login
  Note over JG: @PublicRoute() → guard skipped
  UAC->>+LUS: Passport local-user validate
  LUS->>LUS: Find user by email + compare password
  LUS-->>-UAC: User entity
  UAC->>+LUH: LoginUserCommand(user)
  LUH->>+TS: generateTokenPair(payload)
  TS->>TS: Sign JWT access (15m) + refresh (7d)
  TS->>TS: Hash & persist refresh token
  TS-->>-LUH: { accessToken, refreshToken }
  LUH-->>-UAC: AuthResponseDto
  UAC-->>-C: 200 { accessToken, refreshToken }

Registration + Email Verification Flow

sequenceDiagram
  participant C as Client
  participant UAC as UserAuthController
  participant RUH as RegisterUserHandler
  participant EB as EventBus
  participant URNH as UserRegisteredNotificationHandler
  participant SENH as SendEmailNotificationHandler
  participant Email as Nodemailer

  C->>+UAC: POST /identity/auth/user/register
  UAC->>+RUH: RegisterUserCommand(dto)
  RUH->>RUH: Hash password, create User
  RUH->>RUH: Generate 6-digit code, create VerificationToken
  RUH->>EB: publish(UserRegisteredEvent)
  RUH-->>-UAC: Result.ok()
  UAC-->>-C: 200 { message: "Verification code sent" }

  EB->>+URNH: handle(UserRegisteredEvent)
  URNH->>+SENH: SendEmailNotificationCommand
  SENH->>SENH: Resolve template, render React Email
  SENH->>Email: send({ to, subject, html })
  SENH->>SENH: Persist NotificationLog
  SENH-->>-URNH: Result.ok()
  URNH-->>-EB: done

Token Refresh Flow

sequenceDiagram
  participant C as Client
  participant AC as AuthController
  participant RTH as RefreshTokenHandler
  participant TS as TokenService
  participant DB as RefreshToken Table

  C->>+AC: POST /identity/auth/refresh
  Note over AC: @PublicRoute() → no JWT required
  AC->>+RTH: RefreshTokenCommand(refreshToken)
  RTH->>+TS: rotateTokens(oldRefreshToken)
  TS->>DB: Find token by hash
  TS->>DB: Delete old token
  TS->>TS: Sign new access + refresh tokens
  TS->>DB: Persist new refresh token hash
  TS-->>-RTH: { accessToken, refreshToken }
  RTH-->>-AC: Result.ok(tokens)
  AC-->>-C: 200 { accessToken, refreshToken }

Database Schema Ownership

Each feature module owns a separate PostgreSQL schema and TypeORM connection. No module exposes its TypeOrmModule internals.

graph LR
  subgraph IdentityModule
    IC["TypeORM connection: 'identity'"]
  end

  subgraph NotificationsModule
    NC["TypeORM connection: 'notifications'"]
  end

  subgraph PlansModule
    PC["TypeORM connection: 'plans'"]
  end

  subgraph PostgreSQL
    IS[("identity schema<br/><i>users, agencies, user_agencies,<br/>clients, refresh_tokens,<br/>user_verification_tokens,<br/>client_verification_tokens</i>")]
    NS[("notifications schema<br/><i>notification_logs</i>")]
    PS[("plans schema<br/><i>plans, plan_pricing,<br/>plan_resource_limits</i>")]
  end

  IC --> IS
  NC --> NS
  PC --> PS

  style IS fill:#e8f5e9,stroke:#27ae60
  style NS fill:#e8f5e9,stroke:#27ae60
  style PS fill:#e8f5e9,stroke:#27ae60

Cross-Module Communication

Modules communicate through two complementary mechanisms:

  1. CQRS EventBus — for async integration events (fire-and-forget, no response needed).
  2. Shared-interface gateways — for synchronous cross-module reads where a handler needs an immediate answer from another module's domain.

Integration Events (EventBus)

Event communication is also an observability boundary:

  • Identity publishes integration events with metadata (eventId, correlationId, trace).
  • Notifications consumers create consumer spans linked to identity producer spans.
  • The linkage uses span links (not parent-child) to preserve module independence while enabling end-to-end traceability.
SourceEventListenerAction
IdentityModuleUserRegisteredEventUserRegisteredNotificationHandlerSend verification email
IdentityModuleClientRegisteredEventClientRegisteredNotificationHandlerSend verification email

Shared-Interface Gateways (Synchronous Reads)

For cases where an application-layer handler needs a synchronous answer from another module, the codebase uses a gateway pattern: an interface lives in shared/application/services/, the owner module provides the adapter implementation, and the consumer module imports the owner module to resolve the gateway token.

This pattern produces a bidirectional module-level dependency between IdentityModule and PlansModule, resolved with NestJS forwardRef(). Each adapter depends only on its own module's repositories, so no circular provider construction occurs.

Gateway interfaceDefined in shared/Implemented byConsumed byPurpose
ISubscriptionLimitsGatewaysubscription-limits-gateway.service.interface.tsPlansModule (SubscriptionLimitsGatewayAdapter)IdentityModuleGate 2 (active plans check), Gate 3 (user capacity), Guard 1 (enable-flag pre-condition)
IOrganizationFlagsGatewayorganization-flags-gateway.service.interface.tsIdentityModule (OrganizationFlagsGatewayAdapter)PlansModuleGuard 2 (last-plan deactivation check)
graph LR
  subgraph Shared["shared/application/services/"]
    SLG["ISubscriptionLimitsGateway"]
    OFG["IOrganizationFlagsGateway"]
  end

  subgraph Identity["IdentityModule"]
    IRH["RegisterUserBusiness\nOwnerHandler"]
    IUO["UpdateMyOrganization\nHandler"]
    IOA["OrganizationFlags\nGatewayAdapter"]
  end

  subgraph Plans["PlansModule"]
    PUA["UpdateAgencyPlan\nHandler"]
    PSA["SoftDeleteAgencyPlan\nHandler"]
    PSL["SubscriptionLimits\nGatewayAdapter"]
  end

  Identity -->|"forwardRef imports"| Plans
  Plans -->|"forwardRef imports"| Identity

  IOA -->|implements| OFG
  PSL -->|implements| SLG

  IRH -.->|injects via| SLG
  IUO -.->|injects via| SLG
  PUA -.->|injects via| OFG
  PSA -.->|injects via| OFG

  style Shared fill:#f5f0ff,stroke:#7b68ee
  style Identity fill:#e8f5e9,stroke:#27ae60
  style Plans fill:#e3f2fd,stroke:#1976d2

For the full rationale, see ADR 007 — Bidirectional Gateway Pattern.


Dependency Injection Pattern

All repository and service contracts use Symbol-based tokens with the useExisting pattern:

1. Register concrete class  →  EncryptionService
2. Alias symbol to class    →  { provide: EncryptionServiceKey, useExisting: EncryptionService }
3. Inject via symbol        →  @Inject(EncryptionServiceKey) svc: IEncryptionService

This keeps application-layer code decoupled from infrastructure implementations. Domain interfaces live in domain/repositories/ or application/services/, while implementations live in infrastructure/.