Backend stack ADR
Backend stack (ADR)
Section titled “Backend stack (ADR)”Audience: backend, devops, leadership
Status: specced
Owns: backend
Depends on: System architecture, Ecosystem map, Orchestrator overview, CAP RPL backend
Decision
Section titled “Decision”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).
Why Express
Section titled “Why Express”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.
Express internal kit (required)
Section titled “Express internal kit (required)”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 assertScope — RBAC 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
$lookupjoins exist but are a poor fit for multi-hop reporting and constraints this domain needs.
Context ownership
Section titled “Context ownership”- 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.
Consequences
Section titled “Consequences”- 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.
Status
Section titled “Status”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.