Project Structure
ForgeStack is an Nx monorepo containing the backend services, the shared backend libraries, the frontend, and all infrastructure-as-code.
Top-level layout
.
├── backend/
│ ├── services/
│ │ └── service-1/ NestJS API (auth, notifications, …)
│ └── libs/
│ ├── common/ CQRS, DDD bases, auth, outbox/inbox, otel
│ ├── kafka/ Kafka producer/consumer + event listener
│ ├── mongodb/ Mongo client, base repository, transactions
│ └── redis/ Redis client (cache + pub/sub)
├── frontend/
│ └── public-page/ Next.js 16 app (landing, auth, console, docs)
├── infra/
│ ├── compose/ docker-compose.dev.yml / .prod.yml
│ ├── caddy/ Caddyfile (reverse proxy + TLS)
│ ├── monitoring/ prometheus, grafana, loki, tempo, promtail
│ ├── mongo/ replica-set startup script
│ ├── env/ *.env.prod.example templates
│ └── scripts/ deploy + server-provision scripts
└── eslint/
└── rules/ custom DDD/CQRS architectural lint rules
Inside a service
service-1 is organised by bounded context. Each context owns the full
hexagonal stack:
backend/services/service-1/src/
├── bounded-contexts/
│ ├── auth/
│ │ ├── domain/
│ │ │ ├── aggregates/ user/, email-verification/, …
│ │ │ ├── value-objects/ email.vo.ts, password.vo.ts, …
│ │ │ └── services/ domain services (ports)
│ │ ├── application/
│ │ │ ├── commands/ <name>.command.ts + .command-handler.ts
│ │ │ ├── queries/ <name>.query.ts + .query-handler.ts
│ │ │ └── domain-event-handlers/
│ │ ├── infrastructure/
│ │ │ ├── repositories/ mongodb/, in-memory/
│ │ │ └── services/ external adapters (e.g. google-oauth)
│ │ ├── interfaces/
│ │ │ ├── controllers/ HTTP endpoints
│ │ │ └── integration-events/ Kafka subscribers
│ │ └── auth.module.ts
│ ├── notifications/
│ └── shared/
├── app.module.ts
└── app.config.ts
Each command lives in its own folder next to its handler — for example
application/commands/register-user/register-user.command.ts and
register-user.command-handler.ts. The cqrs-handler-collocation lint rule
enforces that a handler imports its command/query from the same directory.
Shared libraries
The backend libs are the reusable spine every service builds on:
| Library | What it provides |
|---|---|
@libs/nestjs-common | CQRS base classes, SharedAggregate, value-object bases, auth (JWT), outbox & inbox, integration-event bases, transactions, logging, tracing, metrics |
@libs/nestjs-kafka | Kafka producer/consumer, the integration-event publisher and listener |
@libs/nestjs-mongodb | Mongo client module, Base_MongoRepository, transaction support |
@libs/nestjs-redis | Redis client (database, publisher, subscriber) |
TypeScript path aliases
Defined in tsconfig.base.json, so imports stay clean and refactor-safe:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@libs/nestjs-common": ["backend/libs/common/src"],
"@libs/nestjs-kafka": ["backend/libs/kafka/src"],
"@libs/nestjs-mongodb": ["backend/libs/mongodb/src"],
"@libs/nestjs-redis": ["backend/libs/redis/src"],
"@bc/*": ["backend/services/service-1/src/bounded-contexts/*"]
}
}
}
Usage:
import { Base_CommandHandler } from '@libs/nestjs-common';
import { Base_MongoRepository } from '@libs/nestjs-mongodb';
import { User } from '@bc/auth/domain/aggregates/user/user.aggregate';
The @bc/* alias is also what the no-cross-bc-imports rule keys off: an
import like @bc/notifications/... from inside the auth context is flagged at
lint time.
The frontend
frontend/public-page/
├── app/
│ ├── layout.tsx root layout (fonts, i18n, providers)
│ ├── page.tsx landing
│ ├── login/ register/ … auth pages
│ ├── dashboard/ authenticated console (sidebar + topbar)
│ ├── docs/ this documentation (MDX)
│ └── shared/ui/ design-system primitives
├── i18n/ next-intl config
├── messages/ translation catalogues (en + 7 locales)
└── lib/ brand config, utils
See Frontend for the conventions.
Naming conventions
File suffixes are part of the contract — the lint rules use them to apply the right base-class and naming checks:
| Suffix | Must… |
|---|---|
*.aggregate.ts | extend SharedAggregate |
*.vo.ts | implement the value-object interface |
*.command.ts / *.command-handler.ts | pair up; handler extends Base_CommandHandler(Command) |
*.query.ts / *.query-handler.ts | pair up; handler extends Base_QueryHandler(Query)<Response>() |
*.domain-event.ts | extend Base_DomainEvent |
*.integration-event.ts | extend Base_IntegrationEvent |
Class names follow the same shape — handlers end in _CommandHandler,
_QueryHandler, etc. (enforced by handler-naming).