Core Architecture
1. Overview
@nx/core implements a layered architecture built on top of the IGNIS Framework. Every backend microservice in the BANA monorepo extends DefaultApplication from this package, inheriting a consistent set of infrastructure concerns: authentication, CORS, health checks, Swagger documentation, database connectivity, and cross-service communication.
This document covers:
- The layered architecture and how IGNIS primitives map to each layer
- Application lifecycle and the
DefaultApplicationclass - Dependency injection patterns
- Component, controller, repository, and service patterns
- Event-driven architecture (Redis Pub/Sub + BullMQ queues)
- Bootstrap helpers for application and migration entry points
- Cross-service communication via
IdentityNetworkService - Authentication flow (JWT + Basic)
- Package dependency chain
2. Layered Architecture
All backend services follow a five-layer architecture. Each layer has a single responsibility and communicates only with its immediate neighbors.
| Layer | Component | Description |
|---|---|---|
| Application | DefaultApplication | preConfigure() lifecycle · CORS · BodyLimit 100 MB · Swagger Scalar |
| Bootstrap Helpers | bootstrapApplication() · bootstrapMigration() · createAppConfig() · createMigrationProcessLoader() | |
| Built-in Components | HealthCheck GET /health · Swagger Scalar /explorer · Authenticate JWT stateless · Basic → Identity | |
| Package Components | Signal · Payment · Commerce · Finance · Inventory · Search · Asset | |
| Service | Business Services | Domain logic · Validation · Orchestration |
| IdentityNetworkService | signIn() HTTP → Identity service · Cross-service Basic auth delegation | |
| Event Handlers | Redis Pub/Sub — Payment · Onboarding channels · IEventBus · RedisPubSubAdapter | |
| Queue Consumers | BullMQ — 4 systems · 8 types · 3 partitions · Commerce · Finance · Inventory · Sale | |
| BaseSocketEventService | broadcast() · sendToRoom() · sendToClient() · Redis-backed cross-instance delivery | |
| Repository | SoftDeletableRepository | Soft delete · Restore · Default filter deletedAt IS NULL auto-applied |
| DefaultCRUDRepository | find · create · updateById · deleteById · count · withTransaction | |
| Infrastructure | PostgresCoreDataSource | Drizzle ORM · node-postgres Pool · 7 schemas · 55 models |
| RedisConnectionFactory | Cache · Pub/Sub · WebSocket · Single mode · Cluster mode | |
| BullMQ | Scheduler · Confirmation · Processing · 12 queues with hash-based partition routing |
| Layer | IGNIS Base Class | Responsibility |
|---|---|---|
| Application | BaseApplication | Lifecycle management, middleware setup, DI container root |
| Component | BaseComponent | Feature module registration (repositories, services, controllers) |
| Service | BaseService | Business logic, transaction orchestration, external communication |
| Repository | DefaultCRUDRepository | Data access abstraction, CRUD operations, soft-delete |
| DataSource | BaseDataSource | Database driver configuration, connection pooling |
3. Application Lifecycle
3.1 DefaultApplication
Every backend service extends DefaultApplication, which in turn extends the IGNIS BaseApplication. It provides a structured initialization flow via the preConfigure() lifecycle method.
export class DefaultApplication extends BaseApplication {
protected applicationRoles: string[] = [];
preConfigure() {
this.applicationRoles = this.getApplicationRoles();
this.configureDatasources();
this.configureRepositories();
this.configureServices();
this.configureComponents();
this.configureSecurity();
this.configureControllers();
}
async postConfigure() {
// Runs after all components are bound
}
}3.2 Lifecycle Sequence
The following diagram shows the full startup sequence, from the entry point (index.ts) through to a running HTTP server.
3.3 preConfigure() Method Order
The order of method calls inside preConfigure() is significant. Each step depends on the previous one being complete.
| Order | Method | What It Does |
|---|---|---|
| 1 | configureDatasources() | Registers PostgreSQL connection pool (no-op by default, datasources are auto-discovered from glob) |
| 2 | configureRepositories() | Registers repository classes into the DI container (no-op by default, repositories are auto-discovered from glob) |
| 3 | configureServices() | Registers IdentityNetworkService for cross-service auth |
| 4 | configureComponents() | Binds HealthCheckComponent at /health and SwaggerComponent at /doc |
| 5 | configureSecurity() | Configures JWT and Basic authentication strategies via AuthenticateComponent |
| 6 | configureControllers() | Registers HTTP controllers (no-op by default, overridden per package) |
3.4 Middleware Setup
setupMiddlewares() configures two global Hono middlewares:
| Middleware | Configuration |
|---|---|
| CORS | Origin: *, all methods, 86400s max-age, credentials enabled |
| Body Limit | 100 MB max, returns 413 Content Too Large on overflow |
3.5 Overriding in Downstream Packages
Each backend service creates its own Application class that extends DefaultApplication and overrides the relevant configure methods:
// packages/sale/src/application.ts
export class Application extends DefaultApplication {
override preConfigure(): void {
this.configureServices();
this.configureComponents();
this.configureSecurity();
}
override configureComponents(): void {
super.configureComponents(); // HealthCheck + Swagger
this.component(ApplicationSaleComponent);
}
override async postConfigure(): Promise<void> {
// Package-specific post-boot logic
}
}4. Dependency Injection
The IGNIS DI container uses constructor injection with the @inject() decorator. Dependencies are identified by string-based binding keys.
4.1 Injection Pattern
import { inject, BaseService } from '@venizia/ignis';
export class SaleOrderService extends BaseService {
constructor(
@inject({ key: 'repositories.SaleOrderRepository' })
private saleOrderRepository: SaleOrderRepository,
@inject({ key: 'repositories.SaleOrderItemRepository' })
private saleOrderItemRepository: SaleOrderItemRepository,
) {
super({ scope: SaleOrderService.name });
}
}4.2 Binding Key Convention
Binding keys follow the pattern {namespace}.{ClassName}:
| Namespace | Example Key | Registered Via |
|---|---|---|
repositories | repositories.SaleOrderRepository | this.application.repository(SaleOrderRepository) |
services | services.IdentityNetworkService | this.application.service(IdentityNetworkService) |
controllers | controllers.SaleOrderController | this.application.controller(SaleOrderController) |
datasources | datasources.PostgresCoreDataSource | this.application.datasource(PostgresCoreDataSource) |
You can also build keys explicitly using the IGNIS BindingKeys utility:
import { BindingKeys, BindingNamespaces } from '@venizia/ignis';
const key = BindingKeys.build({
namespace: BindingNamespaces.REPOSITORY,
key: 'SaleOrderRepository',
});
// Result: 'repositories.SaleOrderRepository'4.3 Value Binding
For configuration objects, use the bind().toValue() pattern:
this.bind<IHealthCheckOptions>({
key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
}).toValue({
restOptions: { path: '/health' },
});5. Component Pattern
Components are the primary mechanism for organizing feature modules. Each component extends BaseComponent and registers its repositories, services, and controllers during the binding() lifecycle method.
export class ApplicationSaleComponent extends BaseComponent {
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
protected application: BaseApplication,
) {
super({
scope: ApplicationSaleComponent.name,
initDefault: { enable: true, container: application },
bindings: {},
});
}
override async binding(): Promise<void> {
this.application.repository(SaleOrderRepository);
this.application.repository(SaleOrderItemRepository);
this.application.service(SaleOrderService);
this.application.service(CheckoutService);
this.application.controller(SaleOrderController);
}
}A component is registered into the application via:
this.component(ApplicationSaleComponent);5.1 Component Composition
Components can load other components, creating a composition tree:
6. Controller Pattern
6.1 ControllerFactory (Auto CRUD)
The IGNIS ControllerFactory generates a full set of CRUD endpoints from a single declaration. This is the standard pattern for most controllers in BANA.
import { controller, inject, ControllerFactory } from '@venizia/ignis';
@controller({ path: '/sale-orders' })
export class SaleOrderController extends ControllerFactory.defineCrudController({
repository: { name: SaleOrderRepository.name },
authenticate: { strategies: ['jwt', 'basic'] },
controller: { name: 'SaleOrderController', basePath: '/sale-orders' },
entity: () => SaleOrderEntity,
}) {
constructor(
@inject({ key: 'repositories.SaleOrderRepository' })
repository: SaleOrderRepository,
) {
super(repository);
}
}The factory generates the following endpoints:
| Method | Path | Description |
|---|---|---|
GET | /sale-orders | List with filter, pagination, sorting |
GET | /sale-orders/:id | Find by ID |
GET | /sale-orders/count | Count matching filter |
POST | /sale-orders | Create |
PUT | /sale-orders/:id | Update by ID |
DELETE | /sale-orders/:id | Delete by ID (soft delete) |
6.2 Custom Controller Endpoints
Additional endpoints are defined alongside the factory-generated ones by adding methods to the controller class. The IGNIS Filter System is available on all list/count endpoints.
7. Repository and SoftDeletableRepository
7.1 Repository Declaration
Repositories are declared with the @repository decorator, linking a model to a datasource:
import { repository } from '@venizia/ignis';
import { PostgresCoreDataSource } from '@nx/core';
@repository({ dataSource: PostgresCoreDataSource, model: SaleOrderEntity })
export class SaleOrderRepository extends SoftDeletableRepository<
TSaleOrderSchema,
TSaleOrder,
TSaleOrderPersist
> {}7.2 SoftDeletableRepository
All BANA repositories extend SoftDeletableRepository instead of DefaultCRUDRepository. This ensures that DELETE operations set a deletedAt timestamp rather than physically removing rows.
| Method | Behavior |
|---|---|
deleteById(id) | Sets deletedAt = NOW() instead of DELETE FROM |
restoreById(id) | Sets deletedAt = NULL to reverse a soft delete |
find(filter) | Automatically excludes rows where deletedAt IS NOT NULL |
7.3 Database Transactions
Repositories expose the datasource for transaction support:
await this.repository.dataSource.withTransaction(async (tx) => {
await this.saleOrderRepository.create({ data: orderData, options: { transaction: tx } });
await this.saleOrderItemRepository.create({ data: itemData, options: { transaction: tx } });
});7.4 PostgresCoreDataSource
The shared datasource uses node-postgres with Drizzle ORM. It auto-discovers models from @repository bindings via getSchema():
@datasource({ driver: 'node-postgres' })
export class PostgresCoreDataSource extends BaseDataSource<IPostgresDataSourceSettings> {
override configure(): void {
const schema = this.getSchema(); // Auto-discovers @repository models
this.pool = new Pool(this.settings);
this.connector = drizzle({ client: this.pool, schema });
}
}All backend packages re-export PostgresCoreDataSource from their local datasources/index.ts, so repositories always reference the same shared datasource.
8. Event-Driven Architecture
BANA uses two complementary event systems for asynchronous communication between services:
| Mechanism | Technology | Use Case | Delivery |
|---|---|---|---|
| Event Bus | Redis Pub/Sub | Real-time notifications between running services | At-most-once, fire-and-forget |
| Job Queues | BullMQ (Redis) | Reliable async task processing with retry | At-least-once, with retry and persistence |
8.1 Event Flow Diagram
8.2 Event Bus (Redis Pub/Sub)
The RedisPubSubAdapter implements the IEventBus interface defined in @nx/core. It wraps messages in a standardized envelope:
interface IEventBusMessage<T> {
id: string; // Snowflake ID
type: string; // Channel name
publishedAt: string; // ISO timestamp
data: T; // Event payload
}Event Channels defined in @nx/core:
| Channel | Publisher | Consumer | Trigger |
|---|---|---|---|
payment.order.success | Sale Service | Finance, Inventory | Payment completed |
user.seller.registered | Identity Service | Commerce Service | Seller registration |
commerce.initialized | Commerce Service | Finance, Inventory | Merchant onboarding complete |
8.3 Job Queues (BullMQ)
Each queue domain uses 3 partitions (P01, P02, P03) for load distribution. A hash-based partition selector routes jobs to a specific partition based on a key (typically the entity ID).
Queue Definitions defined in @nx/core:
| Queue Domain | Queue Type | Producer | Consumer |
|---|---|---|---|
| Commerce | seller-registered | Identity | Commerce |
| Commerce | commerce-initialized-for-finance | Commerce | Finance |
| Commerce | commerce-initialized-for-inventory | Commerce | Inventory |
| Finance | purchase-order-received | Inventory | Finance |
| Finance | sale-order-completed | Sale | Finance |
| Sale | payment-success-for-finance | Sale | Finance |
| Sale | payment-success-for-inventory | Sale | Inventory |
| Inventory | product_variant_created | Commerce | Inventory |
Queue naming follows the pattern: @nx/{package}/{queue-type}/{partition}
Example: @nx/sale/payment-success-for-finance/01
8.4 WebSocket Events
@nx/core provides utility classes for building WebSocket room and topic identifiers, used by the Signal service:
| Utility | Format | Example |
|---|---|---|
WebSocketRooms.build() | wr:{prefix}/{paths} | wr:observation/merchants/abc-123 |
WebSocketTopics.build() | ws:{paths joined by .} | ws:observation.sale.sale-order |
9. Bootstrap Helpers
@nx/core provides two bootstrap functions that standardize entry points across all packages. See the IGNIS Bootstrapping reference for the underlying framework concepts.
9.1 Application Bootstrap
Used in each package's src/index.ts to start the HTTP server:
// packages/sale/src/index.ts
import { bootstrapApplication } from '@nx/core';
import { Application } from './application';
import { appConfig } from './common/app-config';
import { resolve } from 'node:path';
bootstrapApplication({
ApplicationClass: Application,
config: appConfig,
options: { bannerPath: resolve(__dirname, '../resources/banner.txt') },
});The bootstrap sequence is: new Application() --> init() --> boot() --> start().
9.2 Migration Bootstrap
Used in each package's src/migrate.ts to run database seeds and migrations:
// packages/sale/src/migrate.ts
import { bootstrapMigration } from '@nx/core';
import { Application } from './application';
import { getMigrationProcesses } from './migrations/processes/migration-process';
bootstrapMigration({
ApplicationClass: Application,
getMigrationProcesses,
});9.3 createMigrationProcessLoader
A factory function that creates a migration process getter from a list of seed file paths:
// packages/sale/src/migrations/processes/migration-process.ts
import { createMigrationProcessLoader } from '@nx/core';
export const getMigrationProcesses = createMigrationProcessLoader({
seedPaths: [
'sale-0001-seed-initial-data',
'sale-0002-seed-tracking-types',
],
importFn: (path) => import(`../processes/${path}.js`),
});Each migration process is an object with name, migrateFn, and optional cleanFn. The MigrationHelper class tracks execution status in the database to prevent duplicate runs.
9.4 createAppConfig
Centralizes application configuration so every package uses the same structure:
// packages/sale/src/common/app-config.ts
import { createAppConfig } from '@nx/core';
export const appConfig = createAppConfig();The function reads from environment variables and returns an IApplicationConfigs object with host, port, base path, debug settings, and boot options (glob patterns for auto-discovering datasources and repositories from @nx/core).
10. Cross-Service Communication
10.1 IdentityNetworkService
The only cross-service network client in the system. It extends AxiosNetworkRequest from @venizia/ignis-helpers/axios and communicates with the Identity service for credential verification.
export class IdentityNetworkService extends AxiosNetworkRequest {
constructor() {
super({
name: IdentityNetworkService.name,
networkOptions: {
baseUrl: applicationEnvironment.get<string>(
EnvironmentKeys.APP_ENV_IDENTITY_SERVICE_BASE_URL,
),
},
});
}
async signIn(opts: {
identifier: { scheme: string; value: string };
credential: { scheme: string; value: string };
}) {
const networkService = this.getNetworkService();
const response = await networkService.post({
url: '/auth/sign-in',
body: opts,
});
return response.data;
}
}This service is registered by default in DefaultApplication.configureServices(), making it available to all downstream packages for Basic authentication credential verification.
10.2 Communication Topology
There is no API gateway. Each service handles its own authentication and exposes its HTTP API directly. The only inter-service HTTP call is for Basic auth credential verification.
All other cross-service communication happens through Redis Pub/Sub events or BullMQ queues (see Section 8).
11. Authentication Flow
DefaultApplication.configureSecurity() sets up the IGNIS AuthenticateComponent with two strategies.
11.1 JWT Strategy
Stateless token-based authentication. The service validates the token locally without contacting another service.
| Environment Variable | Purpose |
|---|---|
APP_ENV_APPLICATION_SECRET | Application-level secret |
APP_ENV_JWT_SECRET | JWT signing secret |
APP_ENV_JWT_EXPIRES_IN | Token expiration in seconds |
11.2 Basic Strategy
Delegates credential verification to the Identity service via IdentityNetworkService.signIn():
11.3 Strategy Registration
Both strategies are registered in the AuthenticationStrategyRegistry singleton:
AuthenticationStrategyRegistry.getInstance().register({
container: this,
strategies: [
{ name: Authentication.STRATEGY_JWT, strategy: JWTAuthenticationStrategy },
{ name: Authentication.STRATEGY_BASIC, strategy: BasicAuthenticationStrategy },
],
});Controllers specify which strategies to use in their authenticate configuration:
authenticate: { strategies: ['jwt', 'basic'] }12. Shared Utilities
@nx/core provides several singleton utilities used across all packages.
| Utility | Purpose |
|---|---|
IdGenerator | Snowflake ID generation singleton (wraps IGNIS SnowflakeUidHelper) |
CryptoUtility | AES-256-GCM encryption and HMAC signing using APP_ENV_APPLICATION_SECRET |
useRequestContext() | Extracts authenticated user, roles, and provides response formatting helpers |
@logged decorator | Method-level performance measurement logging |
RedisConnectionFactory | Creates single-mode or cluster-mode Redis connections |
12.1 IdGenerator
import { IdGenerator } from '@nx/core';
const id = IdGenerator.getInstance().nextId();
// Returns: '7193487234817024' (Snowflake ID string)Configured via environment variables:
APP_ENV_SNOWFLAKE_WORKER_ID(0-1023, unique per service instance)APP_ENV_SNOWFLAKE_EPOCH_CHECKPOINT(custom epoch in milliseconds)
12.2 useRequestContext()
A wrapper around the IGNIS useRequestContext that adds BANA-specific fields:
const {
context, // Hono request context
currentUser, // JWT payload with userId, roles
userId, // Shortcut: currentUser.userId
roles, // Shortcut: currentUser.roles[].identifier
isAlwaysAllowed, // true if user has SUPER_ADMIN or ADMIN role
normalizeCountableData, // Format list responses with count/range headers
formatResponse, // Format single-item responses
formatArrayResponse, // Format array responses
} = useRequestContext();13. Package Dependency Chain
All backend packages depend on @nx/core. The following diagram shows the complete dependency tree:
| Package | Direct Dependencies |
|---|---|
@nx/core | None (foundation) |
@nx/asset | @nx/core |
@nx/search | @nx/core |
@nx/inventory | @nx/core |
@nx/identity | @nx/core |
@nx/finance | @nx/core |
@nx/signal | @nx/core |
@nx/payment | @nx/core, @nx/mq-pay |
@nx/sale | @nx/core |
@nx/commerce | @nx/core, @nx/asset, @nx/search, @nx/inventory |
14. Database Schemas
@nx/core defines all Drizzle ORM schemas centrally so that every service accesses the same database structure. Schemas are organized across 7 PostgreSQL schemas:
| PostgreSQL Schema | Model Count | Domain |
|---|---|---|
public | 30 | Users, roles, products, merchants, organizers, configurations |
allocation | 4 | Event seating and venue layouts |
pricing | 7 | Fares, pricing rules, costs, taxes |
inventory | 8 | Stock, purchase orders, vendors, tracking |
finance | 3 | Wallets, transactions, categories |
payment | 1 | Webhook configurations |
sale | 2 | Sale orders and items |
For detailed schema documentation, see Database ERD.
15. Related Documentation
- Core Package Overview -- Package introduction and project structure
- Database ERD -- Entity-relationship diagrams
- Components -- Base classes and reusable components
- Utilities -- Helper functions and utility classes
- Configuration -- Environment and middleware configuration
- IGNIS Framework Reference -- Underlying framework documentation