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 onSWAGGER_ENABLED, JetStream microservice attachment, andSIGINTcleanup that closes cached tenant connections.src/app.module.ts— root module: registersConfigModule,TypeOrmModule,BullModule,HttpModule,JwtModule,RedisCacheModule, the i18n module, theLoggerMiddleware/TenancyMiddlewarechain, and the global filters/interceptor.src/app.controller.tsandsrc/app.service.ts— the placeholderGET /route returningHello World!.src/config/— Joi-validated configuration loaders forapp,database,redis,security(JWT secret and expiry), andjetstream. Anindex.tsaggregates them into the array consumed byConfigModule.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, andtypes/(AuthUser,User, tenant entity interface).src/filters/—http-exception.tsandrpc-exception.tsregistered as global filters.src/helpers/—jwt.ts(thejwtErrorHandlerused byTenancyMiddleware),i18n/validation exception factory, and an aggregatingindex.ts.src/i18n/— translation bundles forenandfr, with TypeScript types generated intosrc/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 aconfig/folder.src/tenant/— the multi-tenant directory entity (tenant.entity-default.ts) mapped to thebusinessestable, plus its response DTO.src/utils/—database/connection.util.ts(cached per-tenant data sources, default data source builder, schema creation helper) and theSnakeNamingStrategy.src/migrations/— referenced from the TypeORM configuration asdist/migrations/*and from npm scripts (migration:run:main,migration:run:tenants); not present in source on this branch.test/—app.e2e-spec.tsandjest-e2e.json.Dockerfile— multi-stage build usingnode:${NODE_VERSION}(theNODE_VERSIONARG is set toyesin the file and would need to be passed as a real version at build time), with separatedeps,build, andfinalstages, running the application as the non-rootnodeuser and exposing port 8000.compose.yaml— local stack with the application, a Postgres container (port5431:5432), Redis 7 (6379:6379), and NATS with JetStream enabled (4222,6222,8222). The application service expectsDATABASE_USER,DATABASE_PASSWORD, andDATABASE_NAMEto 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, thedist/build output, and acoverage/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.