Appearance
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
| Module | Scope | @Global | Key Exports |
|---|---|---|---|
| AppModule | Root orchestrator | No | — |
| SharedModule | Cross-cutting infra services | Yes | HashingServiceKey, EncryptionServiceKey |
| AppConfigModule | Env validation + typed config | Yes | AppConfigService |
| CoreModule | Global HTTP guards + request observability interceptor | Yes | APP_GUARD, APP_INTERCEPTOR |
| IdentityModule | Auth, users, clients, agencies | No | IdentityRepositoryProviders, IdentityInfrastructureAdapters (incl. IOrganizationFlagsGateway) |
| NotificationsModule | Email delivery + logging | No | — |
| PlansModule | Public plans catalog + internal plan CRUD | No | PlansRepositoryProviders, PlansAdapterProviders (incl. ISubscriptionLimitsGateway) |
| CatalogModule | Service configuration catalog CRUD | No | IServiceConfigurationRepositoryKey |
Dormant AI Module Code Path
The repository also contains an AIModule, but it is not part of the mounted dependency graph yet.
AIModulealready creates the dedicatedaiTypeORM connection and registers AI persistence entities.- The module defines controller and provider registries in source, but
ai.module.tsstill leavescontrollersandprovidersempty. AppModuledoes not import or routeAIModule, 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(). RequestObservabilityInterceptorwraps each HTTP request with:- canonical request log (
http.request.completed) - server span (
SpanKind.SERVER) with request metadata - request ID propagation (
x-request-id)
- canonical request log (
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
NodemailerEmailAdapterprefixes 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:
- CQRS EventBus — for async integration events (fire-and-forget, no response needed).
- 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.
| Source | Event | Listener | Action |
|---|---|---|---|
| IdentityModule | UserRegisteredEvent | UserRegisteredNotificationHandler | Send verification email |
| IdentityModule | ClientRegisteredEvent | ClientRegisteredNotificationHandler | Send 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 interface | Defined in shared/ | Implemented by | Consumed by | Purpose |
|---|---|---|---|---|
ISubscriptionLimitsGateway | subscription-limits-gateway.service.interface.ts | PlansModule (SubscriptionLimitsGatewayAdapter) | IdentityModule | Gate 2 (active plans check), Gate 3 (user capacity), Guard 1 (enable-flag pre-condition) |
IOrganizationFlagsGateway | organization-flags-gateway.service.interface.ts | IdentityModule (OrganizationFlagsGatewayAdapter) | PlansModule | Guard 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: IEncryptionServiceThis keeps application-layer code decoupled from infrastructure implementations. Domain interfaces live in domain/repositories/ or application/services/, while implementations live in infrastructure/.