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/.