Skip to content

Backend stack ADR

Audience: backend, devops, leadership
Status: specced
Owns: backend
Depends on: System architecture, Ecosystem map, Orchestrator overview, CAP RPL backend

Elimi HTTP services (Orchestrator, CAP, then LMS / WorkMasters) use:

Layer Choice
Language TypeScript on Node.js
HTTP framework Express
Package manager pnpm
Database PostgreSQL (one database per service)
ORM / migrations Prisma
Cache / OTP TTL / rate limits Redis
Async messaging RabbitMQ + transactional outbox (poller first; Debezium later if needed)
Sync inter-service gRPC (.proto in @yourorg/proto)
Email (notification channel v1) Transactional provider behind a Notification port (e.g. Resend / SES / Postmark)
Observability OpenTelemetry → self-hosted SigNoz
CI/CD GitHub Actions
Deploy Containers; PM2 cluster mode only for multi-core inside a container
Shared kit @yourorg/common — logger, errors, envelope, pagination, ULID, OTel, outbox helpers, RBAC convention; @yourorg/proto — Identity + Payment + LMS Recommendations gRPC
Public contract OpenAPI 3.1 design SoT at ecosystem root; published under openapi/ in this docs repo
Gateway routing Path prefixes: /v1/ol/… (Orchestrator), /v1/cap/… (CAP), /v1/lms/… (LMS); staging www.staging-api.elimi-ecosystem.e-limi.africa, prod api.elimi-ecosystem.e-limi.africa

Not chosen: MongoDB as primary store; BullMQ as the primary cross-service async path; Woodpecker / Gitea Actions as the CI control plane (considered — see CI/CD).

The team is most familiar with Express and prefers its directness. NestJS / Fastify were considered; Express wins on team fluency, not on framework features.

Because Express is under-opinionated, each service must adopt the shared coding standard and kit from day one. See Coding standards.

Shared conventions via @yourorg/common (imported by all four services):

Concern Rule
Layout routes/controllers/services/repositories/; plus events/, grpc/, jobs/, lib/
Errors AppError hierarchy (ValidationError, NotFoundError, ConflictError, UnauthorizedError, ScopeError); single Express error middleware → envelope
Auth JWT verify middleware shared; CAP does not re-implement login
Roles Layer-1 role gate + layer-2 assertScopeRBAC matrix
OpenAPI Ecosystem-root YAML is design SoT; docs publish synced copies; CI fails if routes diverge (or generate stubs from YAML)
Config Env via schema validation (e.g. zod); no scattered process.env reads
Observability OTel bootstrap from shared package — traces, metrics, logs from go
Value objects Money (minor units + currency), PhoneNumber (E.164) where primitives invite bugs

Database choice — PostgreSQL, not MongoDB

Section titled “Database choice — PostgreSQL, not MongoDB”

Stay on Postgres across all four services:

  • CAP workflows (applications → versions → feedback → appeals → assignments) are deeply relational; referential integrity matters.
  • Payments and wallets need ACID transactions.
  • Flexible form/application payloads use JSONB columns with schema_version — no need for a separate document database.
  • MongoDB $lookup joins exist but are a poor fit for multi-hop reporting and constraints this domain needs.
  • Orchestrator: User/auth, JWT issuance, identity verification, notifications, payments, storage
  • CAP: Application lifecycle (RPL + NSQ), Assessor / Centre / AwardingBody, Trade & Unit, onboarding, competency
  • LMS: Courses, entitlements, progress, LMS certificates; CAP calls over gRPC for recommendations; LMS prices course checkout
  • WorkMasters: Artisan portfolios; competency checks against CAP later

Infra detail: System architecture. Observability: Observability. Deploy: CI/CD. Deploy-time CLI: Console commands.

  • Frontend and QA consume OpenAPI from this docs repo before service code lands.
  • Auth OTP flows depend on Notifications (email channel first).
  • Cross-service side effects that must survive crashes go through the outbox → RabbitMQ, not in-process EventEmitters alone.
  • Services deploy independently; event/API changes stay backward-compatible for at least one release.
  • Org CRUD (Assessor, Centre, Awarding Body) is platform-admin scoped in v1.
  • Google sign-in and password + OTP are both in Orchestrator Auth v1.

Accepted. Revisit only if Express kit ownership or messaging patterns fail in practice. Older “deferred RabbitMQ / OTel / containers” language is superseded by this ADR and System architecture.