Skip to content

WMS (Warehouse Management Service)

Overview

The Warehouse Management Service, exposed in this repository as the npm package warehouse-management-system, is the NestJS application Vendease intends to use for warehouse operations across multiple business tenants. The project README describes it as "Vendease's solution for managing warehouse operations in a multi-tenant environment", and the Swagger title configured in src/common/constants/index.ts reads Vendease Warehouse Management System.

In its current state on this branch the codebase is best described as a fully configured platform shell rather than a feature-complete product. The foundations a multi-tenant Postgres NestJS service needs are wired up: a global ConfigModule with Joi-validated configuration files, a TypeOrmModule bootstrapped against a default schema with a snake-case naming strategy, a tenant-aware data-source factory keyed on tenant identifier, JWT-based request identification via a TenancyMiddleware, a Bull queue backed by Redis, a Redis cache module, a NATS JetStream microservice transport for asynchronous events, internationalisation through nestjs-i18n, and Swagger documentation that is exposed at /v1/documentation when enabled. What is not yet present in src/ is the warehouse-domain itself: the only HTTP route currently bound is the default AppController returning Hello World!, and the only persisted entity defined under src/ is the multi-tenant TenantEntity mapped to the businesses table. The Jest configuration in package.json lists coverage exclusions for paths such as src/recurring-order/cron, src/cart/cron and src/invoice/cron, hinting at intended future modules, but those folders do not yet exist on this branch.

The result is a service that boots cleanly, exposes its health surface and shared infrastructure, and is ready to receive warehouse-specific modules (inventory, stock movements, picks, locations, replenishment, and similar domain concepts implied by the project name) without further re-platforming work.

Architecture & Technology

The runtime is Node.js (the README pins >= 18.0.1) running a NestJS 11 application written in TypeScript 5.7. The HTTP layer is Express via @nestjs/platform-express, with helmet enabled outside development and a CORS allow-list scoped to *.vendease.com (and localhost in development). All HTTP routes are mounted under the global prefix v1. A TrimWhitespacePipe and a ValidationPipe configured with { transform: true, whitelist: true } and a custom exception factory enforce DTO validation globally, and two global filters (HttpExceptionFilter and RpcExceptionFilter) normalise error responses for HTTP and microservice transports respectively.

Persistence uses Postgres through the pg driver and TypeORM 0.3, configured in src/config/database.ts. The connection details are loaded from environment variables (DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD) and validated by Joi at startup. The default TypeOrmModule.forRootAsync registration in src/app.module.ts loads entities matching the glob dist/**/*.entity-default.js and migrations under dist/migrations/*. This split between *.entity-default.ts (registered on the shared default connection) and *.entity.ts (registered per tenant in src/utils/database/connection.util.ts) is the central pattern of the multi-tenant model: shared rows such as the businesses directory of tenants live in the default schema, and each tenant's domain data is held in a tenant_<uuid> schema served by its own DataSource.

Tenant isolation is implemented at request time. The TenancyMiddleware (src/common/middlewares/tenancy.ts) inspects the Authorization header, verifies the JWT through @nestjs/jwt, and attaches both the decoded AuthUser and the tenant identifier to the request. The header x-tenant-id is also surfaced as a global Swagger parameter, so an authorised operator can target a specific tenant. The connection helpers getTenantConnection, getTenantDataSource, and getDefaultDataSource provide a per-tenant DataSource cached in an LRUCache<string, DataSource> of capacity 100, which avoids re-initialising connections for hot tenants and allows graceful shutdown via closeConnections() from the SIGINT handler in main.ts.

Background work and inter-service messaging are handled by two complementary pieces. @nestjs/bull is registered with Redis credentials drawn from the redis config, providing the in-process queue mechanism that future cron and worker modules can attach to. NATS JetStream is wired through the @nestjs-plugins/nestjs-nats-jetstream-transport package: src/config/jetstream.ts configures the connection (JETSTREAM_URL), durable consumer (JETSTREAM_CONSUMER_DURABLE, JETSTREAM_CONSUMER_DELIVER_TO), deliver group (JETSTREAM_DELIVER_GROUP) and stream subjects (JETSTREAM_STREAM_SUBJECTS, defaulting to core.*,payment.*), and src/modules/jetstream.ts exposes both a global NatsJetStreamClientProxy provider keyed 'JETSTREAM' and a getJetstreamStrategy() helper that main.ts uses to attach JetStream as a microservice transport via app.connectMicroservice plus app.startAllMicroservices. The default subject filter (core.*, payment.*) makes it clear that the service is being plugged into a broader event bus shared with at least core and payment services.

Cross-cutting concerns include caching via a custom RedisCacheModule, internationalisation via nestjs-i18n with translation bundles under src/i18n/en and src/i18n/fr and a generated typings file at src/generated/i18n.generated.ts, request logging via the LoggerMiddleware, a TranslateInterceptor registered as a global APP_INTERCEPTOR, and HTTP client access through @nestjs/axios. Health probes and graceful shutdown hooks are available through @nestjs/terminus (declared as a dependency, ready to be wired into a health controller).

The AbstractEntity base class in src/common/abstract.entity.ts defines a UUID primary key, createdAt/updatedAt columns, and a deletedTokenId column that is included in the unique index on the businesses table — indicating that the project favours soft-deletion combined with uniqueness by "active row" (the deletion marker rotates on delete so that a name can be reused). Subscribers under src/common/subscribers/ are referenced from the tenant data-source factory and provide a hook point for cross-entity behaviour.

How It Fits With Other Systems

Within the wider Vendease platform, the service is positioned as one node on a NATS JetStream message fabric. The default JetStream subjects (core.*,payment.*) and the deliver group name (core-service-group) imply the service consumes events emitted by the core platform and the payment domain. The microservice topology is centred on NATS for transport, with this service built to be a participant rather than an originator.

Outbound integration is JWT-based. Because the TenancyMiddleware simply verifies the token using a shared JWT_SECRET and trusts a tenantId claim, the service expects to be issued tokens by an upstream auth issuer (likely the core platform). Tenant identifiers are UUIDs, matching the tenant_<uuid> schema convention used by the data-source factory. The CORS allow-list keyed on vendease.com indicates the service is consumed primarily by Vendease web properties.

The service does not, on this branch, integrate with the Odoo workspace that sits next to it in the parent directory; the two systems are independently deployed services that may be peered through the message bus in production but share no compile-time coupling here.

Repository Layout

The top-level repository contains the standard NestJS scaffolding, alongside container and CI files:

  • src/main.ts — bootstrap: CORS allow-list, global pipes/prefix, Swagger setup gated on SWAGGER_ENABLED, JetStream microservice attachment, and SIGINT cleanup that closes cached tenant connections.
  • src/app.module.ts — root module: registers ConfigModule, TypeOrmModule, BullModule, HttpModule, JwtModule, RedisCacheModule, the i18n module, the LoggerMiddleware/TenancyMiddleware chain, and the global filters/interceptor.
  • src/app.controller.ts and src/app.service.ts — the placeholder GET / route returning Hello World!.
  • src/config/ — Joi-validated configuration loaders for app, database, redis, security (JWT secret and expiry), and jetstream. An index.ts aggregates them into the array consumed by ConfigModule.forRoot.
  • src/common/ — shared building blocks: abstract.entity.ts/abstract.dto.ts (UUID + timestamps base classes), constants/ (Swagger title and version strings), interceptors/ (translation interceptor and others), middlewares/ (logger.middleware.ts, tenancy.ts), pipes/ (TrimWhitespacePipe), subscribers/entity.subscriber.ts, and types/ (AuthUser, User, tenant entity interface).
  • src/filters/http-exception.ts and rpc-exception.ts registered as global filters.
  • src/helpers/jwt.ts (the jwtErrorHandler used by TenancyMiddleware), i18n/ validation exception factory, and an aggregating index.ts.
  • src/i18n/ — translation bundles for en and fr, with TypeScript types generated into src/generated/i18n.generated.ts.
  • src/modules/ — infrastructure-flavoured modules rather than domain modules: jetstream.ts (NATS JetStream client and server strategy), redis-cache/ (the global cache wrapper), translation/ (the i18n module factory), and a config/ folder.
  • src/tenant/ — the multi-tenant directory entity (tenant.entity-default.ts) mapped to the businesses table, plus its response DTO.
  • src/utils/database/connection.util.ts (cached per-tenant data sources, default data source builder, schema creation helper) and the SnakeNamingStrategy.
  • src/migrations/ — referenced from the TypeORM configuration as dist/migrations/* and from npm scripts (migration:run:main, migration:run:tenants); not present in source on this branch.
  • test/app.e2e-spec.ts and jest-e2e.json.
  • Dockerfile — multi-stage build using node:${NODE_VERSION} (the NODE_VERSION ARG is set to yes in the file and would need to be passed as a real version at build time), with separate deps, build, and final stages, running the application as the non-root node user and exposing port 8000.
  • compose.yaml — local stack with the application, a Postgres container (port 5431:5432), Redis 7 (6379:6379), and NATS with JetStream enabled (4222, 6222, 8222). The application service expects DATABASE_USER, DATABASE_PASSWORD, and DATABASE_NAME to be present in the host environment.
  • .env-sample, .env, .dockerignore, .gitignore, .prettierrc, eslint.config.mjs, nest-cli.json, tsconfig.json, tsconfig.build.json, package.json, package-lock.json, the dist/ build output, and a coverage/ directory.

Running, Building & Deploying

The local workflow follows the conventions documented in README.md. After installing dependencies with npm install, the application can be started in three modes through the npm scripts in package.json: npm run start for a plain nest start, npm run start:dev for nest start --watch, and npm run start:prod for node dist/main once the build has produced dist/. The build script is npm run build (a thin wrapper around nest build). Code style is governed by npm run lint (ESLint with the @typescript-eslint and unused-imports plugins) and npm run format (Prettier).

Database migrations are split between the shared default schema and per-tenant schemas, exposed as npm run migration:run:main and npm run migration:run:tenants, with npm run seed:run for default-schema seed data. These scripts are referenced in the README but their underlying binaries are expected to live under src/scripts (which the Jest coverage configuration also excludes). They rely on the same DataSource factories as the runtime: getDefaultDataSource for main, and a tenant-iterating script that calls getTenantDataSource(tenantId, true) per tenant returned by getAppTenants(). Schema creation for a new tenant is done through createSchema(name) in connection.util.ts, which wraps QueryRunner.createSchema(name, true).

For local containers, the Docker workflow lives in README.Docker.md and compose.yaml. Running docker compose up --build starts the application, Postgres, Redis, and NATS together; the application listens on http://localhost:8000. The Compose file injects a DATABASE_URL and REDIS_URL (although the runtime configuration reads the discrete DATABASE_* variables defined in src/config/database.ts, so the URL is informational unless those discrete variables are also exported), and forces SWAGGER_ENABLED='true'. The Postgres container publishes on host port 5431 to avoid clashing with a local Postgres on 5432.

For deployment, the README points at a hosted documentation surface (https://wms-dev.vendease.com/v1/documentation), confirming a dev environment exists and that Swagger is exposed there. The Dockerfile multi-stage build is suitable for pushing to any container registry; on Apple-silicon hosts targeting amd64 cloud runtimes, the README.Docker.md notes the --platform=linux/amd64 flag should be used during docker build.

The full set of environment variables the service reads is documented in README.md. Key categories are application identity (NAME, VERSION, NODE_ENV, ENVIRONMENT), HTTP server (SERVER_HOST/HOST, SERVER_PORT/PORT), Swagger and logging (SWAGGER_ENABLED, LOG_LEVEL), Postgres (DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, DATABASE_SYNCHRONIZE, DATABASE_LOGGING), Redis (REDIS_HOST, REDIS_PORT), and NATS JetStream (NATS_HOST, NATS_PORT, JETSTREAM_URL, JETSTREAM_DELIVER_GROUP, JETSTREAM_CONSUMER_DURABLE, JETSTREAM_CONSUMER_DELIVER_TO, JETSTREAM_STREAM_NAME, JETSTREAM_STREAM_SUBJECTS). A JWT_SECRET is required by src/config/security.ts.

Tests are run with npm run test (Jest unit tests over files matching *.spec.ts rooted at src/), npm run test:cov for coverage, and npm run test:e2e against test/jest-e2e.json with --runInBand --forceExit. Coverage excludes the configuration directory, generated DTOs, mocks, controllers, modules, migrations, scripts, common decorators and interceptors, and the bootstrap files — focusing measurement on services and business logic.

Operational Notes & Risks

A few aspects of the current state are worth surfacing for whoever takes the service forward.

The Dockerfile declares ARG NODE_VERSION=yes, which produces an invalid base image tag (node:yes) unless the build is invoked with --build-arg NODE_VERSION=20 (or similar). This is almost certainly a placeholder that should be pinned to a concrete LTS version before any image is built unattended.

The default-config behaviour in src/config/database.ts evaluates synchronize as process.env.DATABASE_SYNCHRONIZE === 'true' || true, which means TypeORM synchronize is effectively always on regardless of the environment variable. In a multi-tenant Postgres deployment with one schema per tenant, an unintended synchronise pass at startup would attempt schema mutations across every connected tenant connection. This default should be tightened to a strict boolean read of the environment variable — the README even states the intended default is false.

The Compose stack in compose.yaml provides a DATABASE_URL and REDIS_URL to the application service, but the application itself reads the discrete DATABASE_* and REDIS_* variables defined in the config files. Local runs that rely solely on the URL form will fall back to the defaults (127.0.0.1, user postgres, password password, database vms-db) and will not reach the Compose db container. The fix is either to export the discrete variables when running Compose or to extend the database config loader to parse a DATABASE_URL.

The bootstrap log line in main.ts reads Server rRunning on … (typo), which is harmless but noticeable in logs. The Swagger title override in the same file assigns to a document reference inside a callback that captures the variable before it is assigned, so the override ((document as any).title = 'RMS') is a latent ordering issue worth a follow-up.

The tenant data-source cache uses an LRUCache of capacity 100. Beyond that threshold, the least-recently-used connection is evicted without an explicit destroy(), which can leak Postgres connections if the workload pattern churns through many tenants. If the production tenant count is expected to exceed this comfortably, the eviction callback should call destroy() on the evicted DataSource.

There is no health controller registered on the service even though @nestjs/terminus is installed. Adding a dedicated health module is a small follow-up that would let upstream load balancers and orchestrators probe liveness and readiness explicitly.

Finally, the code that gives this service its name — the warehouse domain — is not yet present in src/. The infrastructure to host it (multi-tenant TypeORM, JetStream consumers, Bull queues, JWT identity, Swagger) is in place and validated, but until inventory, location, stock movement, and fulfilment modules are added, the service does not yet do warehouse management. Consumers integrating with it through the message bus or HTTP should treat the current build as an integration-ready skeleton rather than a behavioural API.